huskies: merge 682_refactor_decompose_server_src_agents_merge_squash_rs_1346_lines
This commit is contained in:
@@ -0,0 +1,487 @@
|
||||
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_md_only_changes_fails() {
|
||||
use std::fs;
|
||||
use tempfile::tempdir;
|
||||
|
||||
let tmp = tempdir().unwrap();
|
||||
let repo = tmp.path();
|
||||
init_git_repo(repo);
|
||||
|
||||
// Create a feature branch that only moves a .huskies/ file.
|
||||
Command::new("git")
|
||||
.args(["checkout", "-b", "feature/story-md_only_test"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
let sk_dir = repo.join(".huskies/work/2_current");
|
||||
fs::create_dir_all(&sk_dir).unwrap();
|
||||
fs::write(sk_dir.join("md_only_test.md"), "---\nname: Test\n---\n").unwrap();
|
||||
Command::new("git")
|
||||
.args(["add", "."])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
Command::new("git")
|
||||
.args(["commit", "-m", "move story file"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
Command::new("git")
|
||||
.args(["checkout", "master"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
let result = run_squash_merge(repo, "feature/story-md_only_test", "md_only_test").unwrap();
|
||||
|
||||
// The squash merge will commit the .huskies/ file, but should fail because
|
||||
// there are no code changes outside .huskies/.
|
||||
assert!(
|
||||
!result.success,
|
||||
"merge with only .huskies/ changes must fail: {}",
|
||||
result.output
|
||||
);
|
||||
|
||||
// Cleanup should still happen.
|
||||
assert!(
|
||||
!repo.join(".huskies/merge_workspace").exists(),
|
||||
"merge workspace should be cleaned up"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn squash_merge_additive_conflict_both_additions_preserved() {
|
||||
use std::fs;
|
||||
use tempfile::tempdir;
|
||||
|
||||
let tmp = tempdir().unwrap();
|
||||
let repo = tmp.path();
|
||||
init_git_repo(repo);
|
||||
|
||||
// Initial file with a shared base.
|
||||
fs::write(repo.join("module.rs"), "// module\npub fn existing() {}\n").unwrap();
|
||||
Command::new("git")
|
||||
.args(["add", "."])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
Command::new("git")
|
||||
.args(["commit", "-m", "initial module"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// Feature branch: appends feature_fn to the file.
|
||||
Command::new("git")
|
||||
.args(["checkout", "-b", "feature/story-238_additive"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
fs::write(
|
||||
repo.join("module.rs"),
|
||||
"// module\npub fn existing() {}\npub fn feature_fn() {}\n",
|
||||
)
|
||||
.unwrap();
|
||||
Command::new("git")
|
||||
.args(["add", "."])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
Command::new("git")
|
||||
.args(["commit", "-m", "add feature_fn"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// Simulate another branch already merged into master: appends master_fn.
|
||||
Command::new("git")
|
||||
.args(["checkout", "master"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
fs::write(
|
||||
repo.join("module.rs"),
|
||||
"// module\npub fn existing() {}\npub fn master_fn() {}\n",
|
||||
)
|
||||
.unwrap();
|
||||
Command::new("git")
|
||||
.args(["add", "."])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
Command::new("git")
|
||||
.args(["commit", "-m", "add master_fn (another branch merged)"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// Squash-merge the feature branch — conflicts because both appended to the same location.
|
||||
let result = run_squash_merge(repo, "feature/story-238_additive", "238_additive").unwrap();
|
||||
|
||||
// Conflict must be detected and auto-resolved.
|
||||
assert!(result.had_conflicts, "additive conflict should be detected");
|
||||
assert!(
|
||||
result.conflicts_resolved,
|
||||
"additive conflict must be auto-resolved; output:\n{}",
|
||||
result.output
|
||||
);
|
||||
|
||||
// Master must contain both additions without conflict markers.
|
||||
let content = fs::read_to_string(repo.join("module.rs")).unwrap();
|
||||
assert!(
|
||||
!content.contains("<<<<<<<"),
|
||||
"master must not contain conflict markers"
|
||||
);
|
||||
assert!(
|
||||
!content.contains(">>>>>>>"),
|
||||
"master must not contain conflict markers"
|
||||
);
|
||||
assert!(
|
||||
content.contains("feature_fn"),
|
||||
"feature branch addition must be preserved on master"
|
||||
);
|
||||
assert!(
|
||||
content.contains("master_fn"),
|
||||
"master branch addition must be preserved on master"
|
||||
);
|
||||
assert!(
|
||||
content.contains("existing"),
|
||||
"original function must be preserved"
|
||||
);
|
||||
|
||||
// Cleanup: no leftover merge-queue branch or workspace.
|
||||
let branches = Command::new("git")
|
||||
.args(["branch", "--list", "merge-queue/*"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(
|
||||
String::from_utf8_lossy(&branches.stdout).trim().is_empty(),
|
||||
"merge-queue branch must be cleaned up"
|
||||
);
|
||||
assert!(
|
||||
!repo.join(".huskies/merge_workspace").exists(),
|
||||
"merge workspace must be cleaned up"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn squash_merge_conflict_resolved_but_gates_fail_reported_as_failure() {
|
||||
use std::fs;
|
||||
use tempfile::tempdir;
|
||||
|
||||
let tmp = tempdir().unwrap();
|
||||
let repo = tmp.path();
|
||||
init_git_repo(repo);
|
||||
|
||||
// Add a script/test that always fails (quality gate). This must be on
|
||||
// master before the feature branch forks so it doesn't cause its own conflict.
|
||||
let script_dir = repo.join("script");
|
||||
fs::create_dir_all(&script_dir).unwrap();
|
||||
fs::write(script_dir.join("test"), "#!/bin/sh\nexit 1\n").unwrap();
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
std::fs::set_permissions(
|
||||
script_dir.join("test"),
|
||||
std::fs::Permissions::from_mode(0o755),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
fs::write(repo.join("code.txt"), "// base\n").unwrap();
|
||||
Command::new("git")
|
||||
.args(["add", "."])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
Command::new("git")
|
||||
.args(["commit", "-m", "initial with failing script/test"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// Feature branch: appends feature content (creates future conflict point).
|
||||
Command::new("git")
|
||||
.args(["checkout", "-b", "feature/story-238_gates_fail"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
fs::write(repo.join("code.txt"), "// base\nfeature_addition\n").unwrap();
|
||||
Command::new("git")
|
||||
.args(["add", "."])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
Command::new("git")
|
||||
.args(["commit", "-m", "feature addition"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// Master: append different content at same location (creates conflict).
|
||||
Command::new("git")
|
||||
.args(["checkout", "master"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
fs::write(repo.join("code.txt"), "// base\nmaster_addition\n").unwrap();
|
||||
Command::new("git")
|
||||
.args(["add", "."])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
Command::new("git")
|
||||
.args(["commit", "-m", "master addition"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// Squash-merge: conflict detected → auto-resolved → quality gates run → fail.
|
||||
let result = run_squash_merge(repo, "feature/story-238_gates_fail", "238_gates_fail").unwrap();
|
||||
|
||||
assert!(result.had_conflicts, "conflict must be detected");
|
||||
assert!(
|
||||
result.conflicts_resolved,
|
||||
"additive conflict must be auto-resolved"
|
||||
);
|
||||
assert!(
|
||||
!result.gates_passed,
|
||||
"quality gates must fail (script/test exits 1)"
|
||||
);
|
||||
assert!(
|
||||
!result.success,
|
||||
"merge must be reported as failed when gates fail"
|
||||
);
|
||||
assert!(
|
||||
!result.output.is_empty(),
|
||||
"output must contain gate failure details"
|
||||
);
|
||||
|
||||
// Master must NOT have been updated (cherry-pick was blocked by gate failure).
|
||||
let content = fs::read_to_string(repo.join("code.txt")).unwrap();
|
||||
assert!(
|
||||
!content.contains("<<<<<<<"),
|
||||
"master must not contain conflict markers"
|
||||
);
|
||||
// master_addition was the last commit on master; feature_addition must NOT be there.
|
||||
assert!(
|
||||
!content.contains("feature_addition"),
|
||||
"feature code must not land on master when gates fail"
|
||||
);
|
||||
|
||||
// Cleanup must still happen.
|
||||
assert!(
|
||||
!repo.join(".huskies/merge_workspace").exists(),
|
||||
"merge workspace must be cleaned up even on gate failure"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn squash_merge_cleans_up_stale_workspace() {
|
||||
use std::fs;
|
||||
use tempfile::tempdir;
|
||||
|
||||
let tmp = tempdir().unwrap();
|
||||
let repo = tmp.path();
|
||||
init_git_repo(repo);
|
||||
|
||||
// Create a feature branch with a file.
|
||||
Command::new("git")
|
||||
.args(["checkout", "-b", "feature/story-stale_test"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
fs::write(repo.join("stale.txt"), "content\n").unwrap();
|
||||
Command::new("git")
|
||||
.args(["add", "."])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
Command::new("git")
|
||||
.args(["commit", "-m", "feature: stale test"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
Command::new("git")
|
||||
.args(["checkout", "master"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// Simulate a stale merge workspace left from a previous failed merge.
|
||||
let stale_ws = repo.join(".huskies/merge_workspace");
|
||||
fs::create_dir_all(&stale_ws).unwrap();
|
||||
fs::write(stale_ws.join("leftover.txt"), "stale").unwrap();
|
||||
|
||||
// Run the merge — it should clean up the stale workspace first.
|
||||
let result = run_squash_merge(repo, "feature/story-stale_test", "stale_test").unwrap();
|
||||
|
||||
assert!(
|
||||
result.success,
|
||||
"merge should succeed after cleaning up stale workspace: {}",
|
||||
result.output
|
||||
);
|
||||
assert!(
|
||||
!stale_ws.exists(),
|
||||
"stale merge workspace should be cleaned up"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn squash_merge_runs_component_setup_from_project_toml() {
|
||||
use std::fs;
|
||||
use tempfile::tempdir;
|
||||
|
||||
let tmp = tempdir().unwrap();
|
||||
let repo = tmp.path();
|
||||
init_git_repo(repo);
|
||||
|
||||
// Add a .huskies/project.toml with a component whose setup writes a
|
||||
// sentinel file so we can confirm the command ran.
|
||||
let sk_dir = repo.join(".huskies");
|
||||
fs::create_dir_all(&sk_dir).unwrap();
|
||||
fs::write(
|
||||
sk_dir.join("project.toml"),
|
||||
"[[component]]\nname = \"sentinel\"\npath = \".\"\nsetup = [\"touch setup_ran.txt\"]\n",
|
||||
)
|
||||
.unwrap();
|
||||
Command::new("git")
|
||||
.args(["add", "."])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
Command::new("git")
|
||||
.args(["commit", "-m", "add project.toml with component setup"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// Create feature branch with a change.
|
||||
Command::new("git")
|
||||
.args(["checkout", "-b", "feature/story-216_setup_test"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
fs::write(repo.join("feature.txt"), "change").unwrap();
|
||||
Command::new("git")
|
||||
.args(["add", "."])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
Command::new("git")
|
||||
.args(["commit", "-m", "feature work"])
|
||||
.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-216_setup_test", "216_setup_test").unwrap();
|
||||
|
||||
// The output must mention component setup, proving the new code path ran.
|
||||
assert!(
|
||||
result.output.contains("component setup"),
|
||||
"merge output must mention component setup when project.toml has components, got:\n{}",
|
||||
result.output
|
||||
);
|
||||
// The sentinel command must appear in the output.
|
||||
assert!(
|
||||
result.output.contains("sentinel"),
|
||||
"merge output must name the component, got:\n{}",
|
||||
result.output
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn squash_merge_succeeds_without_components_in_project_toml() {
|
||||
use std::fs;
|
||||
use tempfile::tempdir;
|
||||
|
||||
let tmp = tempdir().unwrap();
|
||||
let repo = tmp.path();
|
||||
init_git_repo(repo);
|
||||
|
||||
// No .huskies/project.toml — no component setup.
|
||||
fs::write(repo.join("file.txt"), "initial").unwrap();
|
||||
Command::new("git")
|
||||
.args(["add", "."])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
Command::new("git")
|
||||
.args(["commit", "-m", "initial commit"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
Command::new("git")
|
||||
.args(["checkout", "-b", "feature/story-216_no_components"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
fs::write(repo.join("change.txt"), "change").unwrap();
|
||||
Command::new("git")
|
||||
.args(["add", "."])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
Command::new("git")
|
||||
.args(["commit", "-m", "feature"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
Command::new("git")
|
||||
.args(["checkout", "master"])
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
let result =
|
||||
run_squash_merge(repo, "feature/story-216_no_components", "216_no_components").unwrap();
|
||||
|
||||
// No pnpm or frontend references should appear in the output.
|
||||
assert!(
|
||||
!result.output.contains("pnpm"),
|
||||
"output must not mention pnpm, got:\n{}",
|
||||
result.output
|
||||
);
|
||||
assert!(
|
||||
!result.output.contains("frontend/"),
|
||||
"output must not mention frontend/, got:\n{}",
|
||||
result.output
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user