huskies: merge 1065
This commit is contained in:
@@ -60,13 +60,6 @@ pub fn read_project_local_prompt(project_root: &Path) -> Option<String> {
|
|||||||
sections.push((rel_path, trimmed.to_string()));
|
sections.push((rel_path, trimmed.to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Regenerate the source map so agents always start from a fresh snapshot.
|
|
||||||
// Failure is non-fatal: log it and fall through to whatever is on disk.
|
|
||||||
let map_path = project_root.join(SOURCE_MAP_REL);
|
|
||||||
if let Err(e) = source_map_gen::regenerate_source_map(project_root, &map_path) {
|
|
||||||
crate::slog!("[agents] source-map regen failed (non-fatal): {}", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read source-map.json (after AGENT.md) with a byte cap.
|
// Read source-map.json (after AGENT.md) with a byte cap.
|
||||||
let source_map_content = read_source_map_section(project_root);
|
let source_map_content = read_source_map_section(project_root);
|
||||||
|
|
||||||
@@ -394,86 +387,6 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Regen-on-spawn tests ─────────────────────────────────────────────────
|
|
||||||
|
|
||||||
fn init_git_repo(dir: &Path) {
|
|
||||||
let run = |args: &[&str]| {
|
|
||||||
std::process::Command::new("git")
|
|
||||||
.args(args)
|
|
||||||
.current_dir(dir)
|
|
||||||
.output()
|
|
||||||
.unwrap();
|
|
||||||
};
|
|
||||||
run(&["init"]);
|
|
||||||
run(&["config", "user.email", "test@test.com"]);
|
|
||||||
run(&["config", "user.name", "Test"]);
|
|
||||||
run(&["commit", "--allow-empty", "-m", "init"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Happy path: regen runs successfully and the fresh map is included in the bundle.
|
|
||||||
#[test]
|
|
||||||
fn regen_creates_map_on_coder_spawn() {
|
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
|
||||||
init_git_repo(tmp.path());
|
|
||||||
|
|
||||||
// Write a tracked Rust file so git ls-files has something to index.
|
|
||||||
write_file(
|
|
||||||
tmp.path(),
|
|
||||||
"lib.rs",
|
|
||||||
"//! Module doc.\n\n/// A function.\npub fn hello() {}\n",
|
|
||||||
);
|
|
||||||
std::process::Command::new("git")
|
|
||||||
.args(["add", "lib.rs"])
|
|
||||||
.current_dir(tmp.path())
|
|
||||||
.output()
|
|
||||||
.unwrap();
|
|
||||||
std::process::Command::new("git")
|
|
||||||
.args(["commit", "-m", "add lib.rs"])
|
|
||||||
.current_dir(tmp.path())
|
|
||||||
.output()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Write an orientation file so we get Some back.
|
|
||||||
write_file(tmp.path(), "CLAUDE.md", "agent hints");
|
|
||||||
|
|
||||||
// Map does not exist yet.
|
|
||||||
let map_path = tmp.path().join(SOURCE_MAP_REL);
|
|
||||||
assert!(!map_path.exists(), "map must not exist before spawn");
|
|
||||||
|
|
||||||
let result = read_project_local_prompt(tmp.path());
|
|
||||||
assert!(
|
|
||||||
result.is_some(),
|
|
||||||
"bundle must be Some when CLAUDE.md present"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Regen should have written the map.
|
|
||||||
assert!(
|
|
||||||
map_path.exists(),
|
|
||||||
"regen must have written source-map.json during spawn"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fallback: regen fails (no git repo) but a stale map on disk is still read.
|
|
||||||
#[test]
|
|
||||||
fn regen_fails_stale_map_still_readable() {
|
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
|
||||||
// No git repo — regen will fail with "git ls-files" error.
|
|
||||||
|
|
||||||
write_file(tmp.path(), "CLAUDE.md", "agent hints");
|
|
||||||
// Write a stale map manually.
|
|
||||||
write_file(
|
|
||||||
tmp.path(),
|
|
||||||
SOURCE_MAP_REL,
|
|
||||||
r#"{"stale/entry.rs": ["fn old"]}"#,
|
|
||||||
);
|
|
||||||
|
|
||||||
let result = read_project_local_prompt(tmp.path()).unwrap();
|
|
||||||
assert!(
|
|
||||||
result.contains("stale/entry.rs"),
|
|
||||||
"stale map must still be readable after regen failure: {result}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[allow(clippy::string_slice)] // sm_start is derived from str::find — always a char boundary
|
#[allow(clippy::string_slice)] // sm_start is derived from str::find — always a char boundary
|
||||||
fn source_map_truncated_at_byte_cap() {
|
fn source_map_truncated_at_byte_cap() {
|
||||||
|
|||||||
@@ -361,6 +361,54 @@ pub(crate) fn run_squash_merge(
|
|||||||
"=== Verified: cherry-pick landed on '{base_branch}' with code changes ===\n"
|
"=== Verified: cherry-pick landed on '{base_branch}' with code changes ===\n"
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// ── Regen source-map.json on master after cherry-pick ─────────
|
||||||
|
// Run deterministically on project_root (now on master). Skip the commit
|
||||||
|
// when regen produces no diff (idempotent case). Failure is non-fatal.
|
||||||
|
{
|
||||||
|
let map_path = project_root.join(".huskies").join("source-map.json");
|
||||||
|
let old_content = std::fs::read_to_string(&map_path).ok();
|
||||||
|
match source_map_gen::regenerate_source_map(project_root, &map_path) {
|
||||||
|
Err(e) => {
|
||||||
|
all_output.push_str(&format!(
|
||||||
|
"=== source-map regen failed (non-fatal): {e} ===\n"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(()) => {
|
||||||
|
let new_content = std::fs::read_to_string(&map_path).ok();
|
||||||
|
if old_content != new_content {
|
||||||
|
all_output.push_str("=== source-map.json changed — committing on master ===\n");
|
||||||
|
let _ = Command::new("git")
|
||||||
|
.args(["add", ".huskies/source-map.json"])
|
||||||
|
.current_dir(project_root)
|
||||||
|
.output();
|
||||||
|
match Command::new("git")
|
||||||
|
.args(["commit", "-m", "huskies: regen source-map.json"])
|
||||||
|
.current_dir(project_root)
|
||||||
|
.output()
|
||||||
|
{
|
||||||
|
Ok(c) if c.status.success() => {
|
||||||
|
all_output.push_str("=== source-map.json committed on master ===\n");
|
||||||
|
}
|
||||||
|
Ok(c) => {
|
||||||
|
let stderr = String::from_utf8_lossy(&c.stderr);
|
||||||
|
all_output.push_str(&format!(
|
||||||
|
"=== source-map commit failed (non-fatal): {stderr} ===\n"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
all_output.push_str(&format!(
|
||||||
|
"=== source-map commit error (non-fatal): {e} ===\n"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
all_output
|
||||||
|
.push_str("=== source-map.json unchanged — no follow-up commit ===\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Clean up ──────────────────────────────────────────────────
|
// ── Clean up ──────────────────────────────────────────────────
|
||||||
cleanup_merge_workspace(project_root, &merge_wt_path, &merge_branch);
|
cleanup_merge_workspace(project_root, &merge_wt_path, &merge_branch);
|
||||||
all_output.push_str("=== Merge-queue cleanup complete ===\n");
|
all_output.push_str("=== Merge-queue cleanup complete ===\n");
|
||||||
|
|||||||
@@ -403,6 +403,112 @@ fn squash_merge_runs_component_setup_from_project_toml() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// AC6: the regen+commit step runs on `project_root` (master) only.
|
||||||
|
/// After a successful merge where the source-map changes, `git log --name-only`
|
||||||
|
/// shows a follow-up commit whose diff contains ONLY `.huskies/source-map.json`.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn regen_commit_on_master_touches_only_source_map() {
|
||||||
|
use std::fs;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
let tmp = tempdir().unwrap();
|
||||||
|
let repo = tmp.path();
|
||||||
|
init_git_repo(repo);
|
||||||
|
|
||||||
|
// Put a stale source-map.json on master so regen will produce a different result.
|
||||||
|
let sk_dir = repo.join(".huskies");
|
||||||
|
fs::create_dir_all(&sk_dir).unwrap();
|
||||||
|
fs::write(sk_dir.join("source-map.json"), "{\"stale\": true}\n").unwrap();
|
||||||
|
|
||||||
|
// Add a tracked Rust file so the regenerator has something to index.
|
||||||
|
fs::create_dir_all(repo.join("src")).unwrap();
|
||||||
|
fs::write(
|
||||||
|
repo.join("src/lib.rs"),
|
||||||
|
"//! Library.\n\n/// Says hello.\npub fn hello() {}\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
Command::new("git")
|
||||||
|
.args(["add", "."])
|
||||||
|
.current_dir(repo)
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
Command::new("git")
|
||||||
|
.args(["commit", "-m", "initial with stale source-map"])
|
||||||
|
.current_dir(repo)
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Feature branch: add a new file.
|
||||||
|
Command::new("git")
|
||||||
|
.args(["checkout", "-b", "feature/story-1065_regen_test"])
|
||||||
|
.current_dir(repo)
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
fs::write(
|
||||||
|
repo.join("src/extra.rs"),
|
||||||
|
"//! Extra.\n\n/// Extra fn.\npub fn extra() {}\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
Command::new("git")
|
||||||
|
.args(["add", "."])
|
||||||
|
.current_dir(repo)
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
Command::new("git")
|
||||||
|
.args(["commit", "-m", "add extra.rs"])
|
||||||
|
.current_dir(repo)
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
Command::new("git")
|
||||||
|
.args(["checkout", "master"])
|
||||||
|
.current_dir(repo)
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let result =
|
||||||
|
run_squash_merge(repo, "feature/story-1065_regen_test", "1065_regen_test").unwrap();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
matches!(result, super::MergeResult::Success { .. }),
|
||||||
|
"clean merge must succeed; got: {result:?}"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find the regen commit if one was created.
|
||||||
|
let log_out = Command::new("git")
|
||||||
|
.args(["log", "--oneline", "--name-only"])
|
||||||
|
.current_dir(repo)
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
let log = String::from_utf8_lossy(&log_out.stdout);
|
||||||
|
|
||||||
|
// If a regen commit exists, its diff must contain ONLY the source-map path.
|
||||||
|
if log.contains("huskies: regen source-map.json") {
|
||||||
|
// Extract files changed in the regen commit.
|
||||||
|
let show_out = Command::new("git")
|
||||||
|
.args(["show", "--name-only", "--format=", "HEAD"])
|
||||||
|
.current_dir(repo)
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
let show = String::from_utf8_lossy(&show_out.stdout);
|
||||||
|
|
||||||
|
// If HEAD is the regen commit, its files list must be exactly one entry.
|
||||||
|
let head_msg = Command::new("git")
|
||||||
|
.args(["log", "-1", "--format=%s"])
|
||||||
|
.current_dir(repo)
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
let head_subject = String::from_utf8_lossy(&head_msg.stdout);
|
||||||
|
if head_subject.trim() == "huskies: regen source-map.json" {
|
||||||
|
let changed_files: Vec<&str> = show.lines().filter(|l| !l.is_empty()).collect();
|
||||||
|
assert_eq!(
|
||||||
|
changed_files,
|
||||||
|
vec![".huskies/source-map.json"],
|
||||||
|
"regen commit must touch ONLY .huskies/source-map.json; got: {changed_files:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
#[test]
|
#[test]
|
||||||
fn squash_merge_succeeds_without_components_in_project_toml() {
|
fn squash_merge_succeeds_without_components_in_project_toml() {
|
||||||
|
|||||||
Reference in New Issue
Block a user