Files
huskies/server/src/agents/merge/squash/tests_basic.rs
T

364 lines
11 KiB
Rust
Raw Normal View History

use super::*;
use std::process::Command;
fn init_git_repo(repo: &std::path::Path) {
Command::new("git")
.args(["init"])
.current_dir(repo)
.output()
.unwrap();
Command::new("git")
.args(["config", "user.email", "test@test.com"])
.current_dir(repo)
.output()
.unwrap();
Command::new("git")
.args(["config", "user.name", "Test"])
.current_dir(repo)
.output()
.unwrap();
Command::new("git")
.args(["commit", "--allow-empty", "-m", "init"])
.current_dir(repo)
.output()
.unwrap();
}
#[tokio::test]
async fn squash_merge_uses_merge_queue_no_conflict_markers_on_master() {
use std::fs;
use tempfile::tempdir;
let tmp = tempdir().unwrap();
let repo = tmp.path();
init_git_repo(repo);
// Create a file that will be conflicted on master.
fs::write(repo.join("shared.txt"), "line 1\nline 2\n").unwrap();
Command::new("git")
.args(["add", "."])
.current_dir(repo)
.output()
.unwrap();
Command::new("git")
.args(["commit", "-m", "initial shared file"])
.current_dir(repo)
.output()
.unwrap();
// Create a feature branch that modifies the file.
Command::new("git")
.args(["checkout", "-b", "feature/story-conflict_test"])
.current_dir(repo)
.output()
.unwrap();
fs::write(
repo.join("shared.txt"),
"line 1\nline 2\nfeature addition\n",
)
.unwrap();
Command::new("git")
.args(["add", "."])
.current_dir(repo)
.output()
.unwrap();
Command::new("git")
.args(["commit", "-m", "feature: add line"])
.current_dir(repo)
.output()
.unwrap();
// Switch to master and make a conflicting change.
Command::new("git")
.args(["checkout", "master"])
.current_dir(repo)
.output()
.unwrap();
fs::write(repo.join("shared.txt"), "line 1\nline 2\nmaster addition\n").unwrap();
Command::new("git")
.args(["add", "."])
.current_dir(repo)
.output()
.unwrap();
Command::new("git")
.args(["commit", "-m", "master: add line"])
.current_dir(repo)
.output()
.unwrap();
// Run the squash merge.
let result = run_squash_merge(repo, "feature/story-conflict_test", "conflict_test").unwrap();
// Master should NEVER contain conflict markers, regardless of outcome.
let master_content = fs::read_to_string(repo.join("shared.txt")).unwrap();
assert!(
!master_content.contains("<<<<<<<"),
"master must never contain conflict markers, got:\n{master_content}"
);
assert!(
!master_content.contains(">>>>>>>"),
"master must never contain conflict markers, got:\n{master_content}"
);
// The merge should have had conflicts.
assert!(result.had_conflicts, "should detect conflicts");
// Conflicts should have been auto-resolved (both are simple additions).
if result.conflicts_resolved {
assert!(result.success, "auto-resolved merge should succeed");
assert!(
master_content.contains("master addition"),
"master side should be present"
);
assert!(
master_content.contains("feature addition"),
"feature side should be present"
);
}
// Verify no leftover merge-queue branch.
let branches = Command::new("git")
.args(["branch", "--list", "merge-queue/*"])
.current_dir(repo)
.output()
.unwrap();
let branch_list = String::from_utf8_lossy(&branches.stdout);
assert!(
branch_list.trim().is_empty(),
"merge-queue branch should be cleaned up, got: {branch_list}"
);
// Verify no leftover merge workspace directory.
assert!(
!repo.join(".huskies/merge_workspace").exists(),
"merge workspace should be cleaned up"
);
}
#[tokio::test]
async fn squash_merge_clean_merge_succeeds() {
use std::fs;
use tempfile::tempdir;
let tmp = tempdir().unwrap();
let repo = tmp.path();
init_git_repo(repo);
// Create feature branch with a new file.
Command::new("git")
.args(["checkout", "-b", "feature/story-clean_test"])
.current_dir(repo)
.output()
.unwrap();
fs::write(repo.join("new_file.txt"), "new content").unwrap();
Command::new("git")
.args(["add", "."])
.current_dir(repo)
.output()
.unwrap();
Command::new("git")
.args(["commit", "-m", "add new file"])
.current_dir(repo)
.output()
.unwrap();
// Switch back to master.
Command::new("git")
.args(["checkout", "master"])
.current_dir(repo)
.output()
.unwrap();
let result = run_squash_merge(repo, "feature/story-clean_test", "clean_test").unwrap();
assert!(result.success, "clean merge should succeed");
assert!(
!result.had_conflicts,
"clean merge should have no conflicts"
);
assert!(
!result.conflicts_resolved,
"no conflicts means nothing to resolve"
);
assert!(
repo.join("new_file.txt").exists(),
"merged file should exist on master"
);
}
#[tokio::test]
async fn squash_merge_nonexistent_branch_fails() {
use tempfile::tempdir;
let tmp = tempdir().unwrap();
let repo = tmp.path();
init_git_repo(repo);
let result = run_squash_merge(repo, "feature/story-nope", "nope").unwrap();
assert!(!result.success, "merge of nonexistent branch should fail");
}
#[tokio::test]
async fn squash_merge_succeeds_when_master_diverges() {
use std::fs;
use tempfile::tempdir;
let tmp = tempdir().unwrap();
let repo = tmp.path();
init_git_repo(repo);
// Create an initial file on master.
fs::write(repo.join("base.txt"), "base content\n").unwrap();
Command::new("git")
.args(["add", "."])
.current_dir(repo)
.output()
.unwrap();
Command::new("git")
.args(["commit", "-m", "initial"])
.current_dir(repo)
.output()
.unwrap();
// Create a feature branch with a new file (clean merge, no conflicts).
Command::new("git")
.args(["checkout", "-b", "feature/story-diverge_test"])
.current_dir(repo)
.output()
.unwrap();
fs::write(repo.join("feature.txt"), "feature content\n").unwrap();
Command::new("git")
.args(["add", "."])
.current_dir(repo)
.output()
.unwrap();
Command::new("git")
.args(["commit", "-m", "feature: add file"])
.current_dir(repo)
.output()
.unwrap();
// Switch back to master and simulate a filesystem watcher commit
// (e.g. a pipeline file move) that advances master beyond the point
// where the merge-queue branch will be created.
Command::new("git")
.args(["checkout", "master"])
.current_dir(repo)
.output()
.unwrap();
let sk_dir = repo.join(".huskies/work/4_merge");
fs::create_dir_all(&sk_dir).unwrap();
fs::write(sk_dir.join("diverge_test.md"), "---\nname: test\n---\n").unwrap();
Command::new("git")
.args(["add", "."])
.current_dir(repo)
.output()
.unwrap();
Command::new("git")
.args(["commit", "-m", "huskies: queue diverge_test for merge"])
.current_dir(repo)
.output()
.unwrap();
// Run the squash merge. With the old fast-forward approach, this
// would fail because master diverged. With cherry-pick, it succeeds.
let result = run_squash_merge(repo, "feature/story-diverge_test", "diverge_test").unwrap();
assert!(
result.success,
"squash merge should succeed despite diverged master: {}",
result.output
);
assert!(!result.had_conflicts, "no conflicts expected");
// Verify the feature file landed on master.
assert!(
repo.join("feature.txt").exists(),
"feature file should be on master after cherry-pick"
);
let feature_content = fs::read_to_string(repo.join("feature.txt")).unwrap();
assert_eq!(feature_content, "feature content\n");
// Verify the watcher commit's file is still present.
assert!(
sk_dir.join("diverge_test.md").exists(),
"watcher-committed file should still be on master"
);
// Verify cleanup: no merge-queue branch, no merge workspace.
let branches = Command::new("git")
.args(["branch", "--list", "merge-queue/*"])
.current_dir(repo)
.output()
.unwrap();
let branch_list = String::from_utf8_lossy(&branches.stdout);
assert!(
branch_list.trim().is_empty(),
"merge-queue branch should be cleaned up, got: {branch_list}"
);
assert!(
!repo.join(".huskies/merge_workspace").exists(),
"merge workspace should be cleaned up"
);
}
#[tokio::test]
async fn squash_merge_empty_diff_fails() {
use std::fs;
use tempfile::tempdir;
let tmp = tempdir().unwrap();
let repo = tmp.path();
init_git_repo(repo);
// Create a file on master.
fs::write(repo.join("code.txt"), "content\n").unwrap();
Command::new("git")
.args(["add", "."])
.current_dir(repo)
.output()
.unwrap();
Command::new("git")
.args(["commit", "-m", "add code"])
.current_dir(repo)
.output()
.unwrap();
// Create a feature branch with NO additional changes (empty diff).
Command::new("git")
.args(["checkout", "-b", "feature/story-empty_test"])
.current_dir(repo)
.output()
.unwrap();
Command::new("git")
.args(["checkout", "master"])
.current_dir(repo)
.output()
.unwrap();
let result = run_squash_merge(repo, "feature/story-empty_test", "empty_test");
// Bug 226 / 675: a zero-commit branch must not be treated as success.
// The pre-flight check (bug 675) returns Err for zero commits ahead;
// the older code path returned Ok(SquashMergeResult { success: false }).
// Either form is a failure — just not success.
match result {
Ok(r) => assert!(
!r.success,
"empty diff merge must fail, not silently succeed: {}",
r.output
),
Err(e) => assert!(
e.contains("no commits to merge") || e.contains("nothing to commit"),
"unexpected error: {e}"
),
}
// Cleanup should still happen (no workspace was created for the Err path).
assert!(
!repo.join(".huskies/merge_workspace").exists(),
"merge workspace should be cleaned up"
);
}