story-kit: create 155_story_queue_messages_while_agent_is_busy

This commit is contained in:
Dave
2026-02-24 15:55:36 +00:00
parent 774548c04c
commit a6d084be31
2 changed files with 277 additions and 0 deletions

View File

@@ -2538,6 +2538,85 @@ fn run_squash_merge(
});
}
// ── Install frontend dependencies for quality gates ──────────────
let frontend_dir_for_install = merge_wt_path.join("frontend");
if frontend_dir_for_install.exists() {
// Ensure frontend/dist/ exists so cargo clippy (RustEmbed) can compile
// even before `pnpm build` has run.
let dist_dir = frontend_dir_for_install.join("dist");
std::fs::create_dir_all(&dist_dir)
.map_err(|e| format!("Failed to create frontend/dist: {e}"))?;
all_output.push_str("=== pnpm install (merge worktree) ===\n");
let pnpm_install = Command::new("pnpm")
.args(["install"])
.current_dir(&frontend_dir_for_install)
.output()
.map_err(|e| format!("Failed to run pnpm install: {e}"))?;
let install_out = format!(
"{}{}",
String::from_utf8_lossy(&pnpm_install.stdout),
String::from_utf8_lossy(&pnpm_install.stderr)
);
all_output.push_str(&install_out);
all_output.push('\n');
if !pnpm_install.status.success() {
all_output.push_str("=== pnpm install FAILED — aborting merge ===\n");
cleanup_merge_workspace(project_root, &merge_wt_path, &merge_branch);
return Ok(SquashMergeResult {
success: false,
had_conflicts,
conflicts_resolved,
conflict_details,
output: all_output,
gates_passed: false,
});
}
}
// ── Install frontend dependencies for quality gates ──────────
let frontend_dir = merge_wt_path.join("frontend");
if frontend_dir.exists() {
// Ensure frontend/dist exists so RustEmbed (cargo clippy) doesn't fail
// even before pnpm build runs.
let dist_dir = frontend_dir.join("dist");
if !dist_dir.exists() {
let _ = std::fs::create_dir_all(&dist_dir);
}
all_output.push_str("=== pnpm install (merge worktree) ===\n");
let pnpm_install = Command::new("pnpm")
.args(["install", "--frozen-lockfile"])
.current_dir(&frontend_dir)
.output()
.map_err(|e| format!("Failed to run pnpm install: {e}"))?;
let install_out = format!(
"{}{}",
String::from_utf8_lossy(&pnpm_install.stdout),
String::from_utf8_lossy(&pnpm_install.stderr)
);
all_output.push_str(&install_out);
all_output.push('\n');
if !pnpm_install.status.success() {
all_output.push_str(
"=== pnpm install FAILED — aborting merge, master unchanged ===\n",
);
cleanup_merge_workspace(project_root, &merge_wt_path, &merge_branch);
return Ok(SquashMergeResult {
success: false,
had_conflicts,
conflicts_resolved,
conflict_details,
output: all_output,
gates_passed: false,
});
}
}
// ── Quality gates in merge workspace (BEFORE fast-forward) ────
// Run gates in the merge worktree so that failures abort before master moves.
all_output.push_str("=== Running quality gates before fast-forward ===\n");
@@ -5568,4 +5647,180 @@ theirs
// Story file should be in 5_archived/
assert!(root.join(".story_kit/work/5_archived/60_story_cleanup.md").exists());
}
// ── bug 154: merge worktree installs frontend deps ────────────────────
/// When the feature branch has a `frontend/` directory, `run_squash_merge`
/// must run `pnpm install` in the merge worktree before quality gates.
/// This test creates a repo with a `frontend/package.json` and verifies that
/// the merge output mentions the pnpm install step.
#[cfg(unix)]
#[test]
fn squash_merge_runs_pnpm_install_when_frontend_exists() {
use std::fs;
use tempfile::tempdir;
let tmp = tempdir().unwrap();
let repo = tmp.path();
init_git_repo(repo);
// Add a frontend/ directory with a minimal package.json on master.
let frontend = repo.join("frontend");
fs::create_dir_all(&frontend).unwrap();
fs::write(
frontend.join("package.json"),
r#"{"name":"test","version":"0.0.0","private":true}"#,
)
.unwrap();
Command::new("git")
.args(["add", "."])
.current_dir(repo)
.output()
.unwrap();
Command::new("git")
.args(["commit", "-m", "add frontend dir"])
.current_dir(repo)
.output()
.unwrap();
// Create feature branch with a change.
Command::new("git")
.args(["checkout", "-b", "feature/story-154_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-154_test", "154_test").unwrap();
// The output must mention pnpm install, proving the new code path ran.
assert!(
result.output.contains("pnpm install"),
"merge output must mention pnpm install when frontend/ exists, got:\n{}",
result.output
);
}
/// When `pnpm install` fails in the merge worktree (e.g. no lockfile),
/// the merge must abort cleanly — success=false, workspace cleaned up.
#[cfg(unix)]
#[test]
fn squash_merge_aborts_when_pnpm_install_fails() {
use std::fs;
use tempfile::tempdir;
let tmp = tempdir().unwrap();
let repo = tmp.path();
init_git_repo(repo);
// Add a frontend/ directory with an invalid package.json that will
// cause pnpm install --frozen-lockfile to fail (no lockfile present).
let frontend = repo.join("frontend");
fs::create_dir_all(&frontend).unwrap();
fs::write(
frontend.join("package.json"),
r#"{"name":"test","version":"0.0.0","dependencies":{"nonexistent-pkg-xyz":"99.99.99"}}"#,
)
.unwrap();
Command::new("git")
.args(["add", "."])
.current_dir(repo)
.output()
.unwrap();
Command::new("git")
.args(["commit", "-m", "add frontend with bad deps"])
.current_dir(repo)
.output()
.unwrap();
// Feature branch with a change.
Command::new("git")
.args(["checkout", "-b", "feature/story-154_fail"])
.current_dir(repo)
.output()
.unwrap();
fs::write(repo.join("change.txt"), "feature").unwrap();
Command::new("git")
.args(["add", "."])
.current_dir(repo)
.output()
.unwrap();
Command::new("git")
.args(["commit", "-m", "feature"])
.current_dir(repo)
.output()
.unwrap();
// Switch back to master, record HEAD.
Command::new("git")
.args(["checkout", "master"])
.current_dir(repo)
.output()
.unwrap();
let head_before = String::from_utf8(
Command::new("git")
.args(["rev-parse", "HEAD"])
.current_dir(repo)
.output()
.unwrap()
.stdout,
)
.unwrap()
.trim()
.to_string();
let result =
run_squash_merge(repo, "feature/story-154_fail", "154_fail").unwrap();
// pnpm install --frozen-lockfile should fail (no lockfile), merge aborted.
assert!(
!result.success,
"merge should fail when pnpm install fails"
);
assert!(
result.output.contains("pnpm install"),
"output should mention pnpm install"
);
// Master HEAD must not have moved.
let head_after = String::from_utf8(
Command::new("git")
.args(["rev-parse", "HEAD"])
.current_dir(repo)
.output()
.unwrap()
.stdout,
)
.unwrap()
.trim()
.to_string();
assert_eq!(
head_before, head_after,
"master HEAD must not advance when pnpm install fails (bug 154)"
);
// Merge workspace should be cleaned up.
assert!(
!repo.join(".story_kit/merge_workspace").exists(),
"merge workspace should be cleaned up after failure"
);
}
}