huskies: merge 645_bug_agent_runtime_panics_with_output_write_bytes_is_ok_assertion_marking_stories_falsely_blocked
This commit is contained in:
@@ -232,7 +232,31 @@ pub(in crate::agents::pool) async fn run_server_owned_completion(
|
||||
let (gates_passed, gate_output) = if let Some(wt_path) = worktree_path {
|
||||
let path = wt_path;
|
||||
match tokio::task::spawn_blocking(move || {
|
||||
crate::agents::gates::check_uncommitted_changes(&path)?;
|
||||
// If the worktree is dirty, check whether committed work survived.
|
||||
// An agent crash (e.g. Claude Code CLI's `output.write(&bytes).is_ok()`
|
||||
// assertion — bug 645) can leave uncommitted files behind even though
|
||||
// the agent already committed valid work. In that case, clean up the
|
||||
// dirty files and proceed with gates on the committed code.
|
||||
if let Err(dirty_msg) = crate::agents::gates::check_uncommitted_changes(&path) {
|
||||
if crate::agents::gates::worktree_has_committed_work(&path) {
|
||||
crate::slog!(
|
||||
"[agents] Worktree dirty but committed work exists — \
|
||||
resetting uncommitted changes and proceeding with gates. \
|
||||
Dirty state: {dirty_msg}"
|
||||
);
|
||||
// Reset dirty files so gates run against committed code only.
|
||||
let _ = std::process::Command::new("git")
|
||||
.args(["checkout", "."])
|
||||
.current_dir(&path)
|
||||
.output();
|
||||
let _ = std::process::Command::new("git")
|
||||
.args(["clean", "-fd"])
|
||||
.current_dir(&path)
|
||||
.output();
|
||||
} else {
|
||||
return Ok((false, dirty_msg));
|
||||
}
|
||||
}
|
||||
// AC5: Fail early if the coder finished with no commits on the feature branch.
|
||||
// This prevents empty-diff stories from advancing through QA to merge.
|
||||
if !crate::agents::gates::worktree_has_committed_work(&path) {
|
||||
@@ -642,4 +666,82 @@ mod tests {
|
||||
"Agent must remain in pool — run_server_owned_completion is a no-op for mergemaster"
|
||||
);
|
||||
}
|
||||
|
||||
/// Bug 645: when an agent crashes leaving dirty files but committed work,
|
||||
/// server-owned completion should clean up the dirty files and run gates
|
||||
/// on the committed code instead of failing immediately.
|
||||
#[tokio::test]
|
||||
async fn server_owned_completion_cleans_dirty_worktree_with_committed_work() {
|
||||
use std::fs;
|
||||
use tempfile::tempdir;
|
||||
|
||||
let tmp = tempdir().unwrap();
|
||||
let project_root = tmp.path().join("project");
|
||||
fs::create_dir_all(&project_root).unwrap();
|
||||
init_git_repo(&project_root);
|
||||
|
||||
// Create a worktree on a feature branch with committed code.
|
||||
let wt_path = tmp.path().join("wt");
|
||||
Command::new("git")
|
||||
.args([
|
||||
"worktree",
|
||||
"add",
|
||||
&wt_path.to_string_lossy(),
|
||||
"-b",
|
||||
"feature/story-645_test",
|
||||
])
|
||||
.current_dir(&project_root)
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// Commit a valid file.
|
||||
fs::write(wt_path.join("work.txt"), "done").unwrap();
|
||||
Command::new("git")
|
||||
.args(["add", "."])
|
||||
.current_dir(&wt_path)
|
||||
.output()
|
||||
.unwrap();
|
||||
Command::new("git")
|
||||
.args(["commit", "-m", "coder: add work"])
|
||||
.current_dir(&wt_path)
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// Now simulate crash leaving dirty files.
|
||||
fs::write(wt_path.join("dirty.txt"), "crash residue").unwrap();
|
||||
|
||||
let pool = AgentPool::new_test(3001);
|
||||
pool.inject_test_agent_with_path(
|
||||
"645_test",
|
||||
"coder-1",
|
||||
AgentStatus::Running,
|
||||
wt_path.clone(),
|
||||
);
|
||||
|
||||
let mut rx = pool.subscribe("645_test", "coder-1").unwrap();
|
||||
|
||||
run_server_owned_completion(
|
||||
&pool.agents,
|
||||
pool.port,
|
||||
"645_test",
|
||||
"coder-1",
|
||||
Some("sess-645".to_string()),
|
||||
pool.watcher_tx.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
// The dirty file should have been cleaned up.
|
||||
assert!(
|
||||
!wt_path.join("dirty.txt").exists(),
|
||||
"dirty file should be cleaned up after server-owned completion"
|
||||
);
|
||||
|
||||
// A Done event should have been emitted (completion ran, didn't fail
|
||||
// on dirty worktree).
|
||||
let event = rx.try_recv().expect("should emit Done event");
|
||||
assert!(
|
||||
matches!(event, AgentEvent::Done { .. }),
|
||||
"expected Done event, got: {event:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user