storkit: merge 372_story_scaffold_auto_detects_tech_stack_and_configures_script_test

This commit is contained in:
dave
2026-03-23 14:23:03 +00:00
parent fffdd5c5ea
commit 96bedd70dc

View File

@@ -110,7 +110,7 @@ role = "Full-stack engineer. Implements features across all components."
model = "sonnet" model = "sonnet"
max_turns = 50 max_turns = 50
max_budget_usd = 5.00 max_budget_usd = 5.00
prompt = "You are working in a git worktree on story {{story_id}}. Read CLAUDE.md first, then .storkit/README.md to understand the dev process. Follow the workflow through implementation and verification. The worktree and feature branch already exist - do not create them. Check .mcp.json for MCP tools. Do NOT accept the story or merge - commit your work and stop.\n\nIMPORTANT: Commit all your work before your process exits. The server will automatically run acceptance gates when your process exits." prompt = "You are working in a git worktree on story {{story_id}}. Read CLAUDE.md first, then .storkit/README.md to understand the dev process. Follow the workflow through implementation and verification. The worktree and feature branch already exist - do not create them. Check .mcp.json for MCP tools. Do NOT accept the story or merge - commit your work and stop.\n\nIMPORTANT: Commit all your work before your process exits. The server will automatically run acceptance gates when your process exits.\n\nIf `script/test` still contains the generic 'No tests configured' stub, update it to run the project's actual test suite before starting implementation."
system_prompt = "You are a full-stack engineer working autonomously in a git worktree. Follow the Story-Driven Test Workflow strictly. Commit all your work before finishing. Do not accept stories, move them to archived, or merge to master." system_prompt = "You are a full-stack engineer working autonomously in a git worktree. Follow the Story-Driven Test Workflow strictly. Commit all your work before finishing. Do not accept stories, move them to archived, or merge to master."
[[agent]] [[agent]]
@@ -215,6 +215,47 @@ pub fn detect_components_toml(root: &Path) -> String {
sections.join("\n") sections.join("\n")
} }
/// Generate `script/test` content for a new project at `root`.
///
/// Inspects well-known marker files to identify which tech stacks are present
/// and emits the appropriate test commands. Multi-stack projects get combined
/// commands run sequentially. Falls back to the generic stub when no markers
/// are found so the scaffold is always valid.
pub fn detect_script_test(root: &Path) -> String {
let mut commands: Vec<&str> = Vec::new();
if root.join("Cargo.toml").exists() {
commands.push("cargo test");
}
if root.join("package.json").exists() {
if root.join("pnpm-lock.yaml").exists() {
commands.push("pnpm test");
} else {
commands.push("npm test");
}
}
if root.join("pyproject.toml").exists() || root.join("requirements.txt").exists() {
commands.push("pytest");
}
if root.join("go.mod").exists() {
commands.push("go test ./...");
}
if commands.is_empty() {
return STORY_KIT_SCRIPT_TEST.to_string();
}
let mut script = "#!/usr/bin/env bash\nset -euo pipefail\n\n".to_string();
for cmd in commands {
script.push_str(cmd);
script.push('\n');
}
script
}
/// Generate a complete `project.toml` for a new project at `root`. /// Generate a complete `project.toml` for a new project at `root`.
/// ///
/// Detects the tech stack via [`detect_components_toml`] and prepends the /// Detects the tech stack via [`detect_components_toml`] and prepends the
@@ -442,7 +483,8 @@ fn scaffold_story_kit(root: &Path, port: u16) -> Result<(), String> {
write_file_if_missing(&story_kit_root.join("project.toml"), &project_toml_content)?; write_file_if_missing(&story_kit_root.join("project.toml"), &project_toml_content)?;
write_file_if_missing(&specs_root.join("00_CONTEXT.md"), STORY_KIT_CONTEXT)?; write_file_if_missing(&specs_root.join("00_CONTEXT.md"), STORY_KIT_CONTEXT)?;
write_file_if_missing(&tech_root.join("STACK.md"), STORY_KIT_STACK)?; write_file_if_missing(&tech_root.join("STACK.md"), STORY_KIT_STACK)?;
write_script_if_missing(&script_root.join("test"), STORY_KIT_SCRIPT_TEST)?; let script_test_content = detect_script_test(root);
write_script_if_missing(&script_root.join("test"), &script_test_content)?;
write_file_if_missing(&root.join("CLAUDE.md"), STORY_KIT_CLAUDE_MD)?; write_file_if_missing(&root.join("CLAUDE.md"), STORY_KIT_CLAUDE_MD)?;
// Write .mcp.json at the project root so agents can find the MCP server. // Write .mcp.json at the project root so agents can find the MCP server.
@@ -1652,6 +1694,124 @@ mod tests {
assert!(!toml.contains("name = \"app\"")); assert!(!toml.contains("name = \"app\""));
} }
// --- detect_script_test ---
#[test]
fn detect_script_test_no_markers_returns_stub() {
let dir = tempdir().unwrap();
let script = detect_script_test(dir.path());
assert!(
script.contains("No tests configured"),
"fallback should contain the generic stub message"
);
assert!(script.starts_with("#!/usr/bin/env bash"));
}
#[test]
fn detect_script_test_cargo_toml_adds_cargo_test() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("Cargo.toml"), "[package]\nname = \"x\"\n").unwrap();
let script = detect_script_test(dir.path());
assert!(script.contains("cargo test"), "Rust project should run cargo test");
assert!(!script.contains("No tests configured"));
}
#[test]
fn detect_script_test_package_json_npm_adds_npm_test() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("package.json"), "{}").unwrap();
let script = detect_script_test(dir.path());
assert!(script.contains("npm test"), "Node project without pnpm-lock should run npm test");
assert!(!script.contains("No tests configured"));
}
#[test]
fn detect_script_test_package_json_pnpm_adds_pnpm_test() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("package.json"), "{}").unwrap();
fs::write(dir.path().join("pnpm-lock.yaml"), "").unwrap();
let script = detect_script_test(dir.path());
assert!(script.contains("pnpm test"), "Node project with pnpm-lock should run pnpm test");
// "pnpm test" is a substring of itself; verify there's no bare "npm test" line
assert!(!script.lines().any(|l| l.trim() == "npm test"), "should not use npm when pnpm-lock.yaml is present");
}
#[test]
fn detect_script_test_pyproject_toml_adds_pytest() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("pyproject.toml"), "[project]\nname = \"x\"\n").unwrap();
let script = detect_script_test(dir.path());
assert!(script.contains("pytest"), "Python project should run pytest");
assert!(!script.contains("No tests configured"));
}
#[test]
fn detect_script_test_requirements_txt_adds_pytest() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("requirements.txt"), "flask\n").unwrap();
let script = detect_script_test(dir.path());
assert!(script.contains("pytest"), "Python project (requirements.txt) should run pytest");
}
#[test]
fn detect_script_test_go_mod_adds_go_test() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("go.mod"), "module example.com/app\n").unwrap();
let script = detect_script_test(dir.path());
assert!(script.contains("go test ./..."), "Go project should run go test ./...");
assert!(!script.contains("No tests configured"));
}
#[test]
fn detect_script_test_multi_stack_combines_commands() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("go.mod"), "module example.com/app\n").unwrap();
fs::write(dir.path().join("package.json"), "{}").unwrap();
let script = detect_script_test(dir.path());
assert!(script.contains("go test ./..."), "multi-stack should include Go test command");
assert!(script.contains("npm test"), "multi-stack should include Node test command");
}
#[test]
fn detect_script_test_output_starts_with_shebang() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("Cargo.toml"), "[package]\nname = \"x\"\n").unwrap();
let script = detect_script_test(dir.path());
assert!(
script.starts_with("#!/usr/bin/env bash\nset -euo pipefail\n"),
"generated script should start with bash shebang and set -euo pipefail"
);
}
#[test]
fn scaffold_script_test_contains_detected_commands_for_rust() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("Cargo.toml"), "[package]\nname = \"myapp\"\n").unwrap();
scaffold_story_kit(dir.path(), 3001).unwrap();
let content = fs::read_to_string(dir.path().join("script/test")).unwrap();
assert!(content.contains("cargo test"), "Rust project scaffold should set cargo test in script/test");
assert!(!content.contains("No tests configured"), "should not use stub when stack is detected");
}
#[test]
fn scaffold_script_test_fallback_stub_when_no_stack() {
let dir = tempdir().unwrap();
scaffold_story_kit(dir.path(), 3001).unwrap();
let content = fs::read_to_string(dir.path().join("script/test")).unwrap();
assert!(content.contains("No tests configured"), "unknown stack should use the generic stub");
}
// --- generate_project_toml --- // --- generate_project_toml ---
#[test] #[test]