diff --git a/server/src/io/fs.rs b/server/src/io/fs.rs index 1557bf4..1d78da4 100644 --- a/server/src/io/fs.rs +++ b/server/src/io/fs.rs @@ -110,7 +110,7 @@ role = "Full-stack engineer. Implements features across all components." model = "sonnet" max_turns = 50 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." [[agent]] @@ -215,6 +215,47 @@ pub fn detect_components_toml(root: &Path) -> String { 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`. /// /// 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(&specs_root.join("00_CONTEXT.md"), STORY_KIT_CONTEXT)?; 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 .mcp.json at the project root so agents can find the MCP server. @@ -1652,6 +1694,124 @@ mod tests { 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 --- #[test]