From 61502f51d9cea8a9017db44312d18f1c733b42ed Mon Sep 17 00:00:00 2001 From: dave Date: Wed, 15 Apr 2026 23:53:03 +0000 Subject: [PATCH] huskies: merge 588_bug_wizard_generated_script_test_misses_frontend_tests_for_projects_with_a_frontend --- server/src/io/fs/scaffold.rs | 185 +++++++++++++++++++++++++++++++++-- 1 file changed, 178 insertions(+), 7 deletions(-) diff --git a/server/src/io/fs/scaffold.rs b/server/src/io/fs/scaffold.rs index 75cd3700..6f70b79f 100644 --- a/server/src/io/fs/scaffold.rs +++ b/server/src/io/fs/scaffold.rs @@ -199,33 +199,69 @@ pub fn detect_components_toml(root: &Path) -> String { sections.join("\n") } +/// Detect the appropriate Node.js test command for a directory containing `package.json`. +/// +/// Reads the `package.json` content to identify known test runners (vitest, jest). +/// Falls back to `npm test` or `pnpm test` based on which lock file is present. +fn detect_node_test_cmd(pkg_dir: &Path) -> String { + let has_pnpm = pkg_dir.join("pnpm-lock.yaml").exists(); + let content = std::fs::read_to_string(pkg_dir.join("package.json")).unwrap_or_default(); + + if content.contains("\"vitest\"") { + let pm = if has_pnpm { "pnpm" } else { "npx" }; + return format!("{} vitest run", pm); + } + if content.contains("\"jest\"") { + let pm = if has_pnpm { "pnpm" } else { "npx" }; + return format!("{} jest", pm); + } + + if has_pnpm { + "pnpm test".to_string() + } else { + "npm test".to_string() + } +} + /// 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. +/// +/// For projects with a frontend in a known subdirectory (`frontend/`, `client/`), +/// the test runner is detected from the `package.json` (vitest, jest, npm, pnpm). pub fn detect_script_test(root: &Path) -> String { - let mut commands: Vec<&str> = Vec::new(); + let mut commands: Vec = Vec::new(); if root.join("Cargo.toml").exists() { - commands.push("cargo test"); + commands.push("cargo test".to_string()); } if root.join("package.json").exists() { if root.join("pnpm-lock.yaml").exists() { - commands.push("pnpm test"); + commands.push("pnpm test".to_string()); } else { - commands.push("npm test"); + commands.push("npm test".to_string()); + } + } + + // Detect frontend in known subdirectories (e.g. frontend/, client/) + for subdir in &["frontend", "client"] { + let sub_path = root.join(subdir); + if sub_path.join("package.json").exists() { + let cmd = detect_node_test_cmd(&sub_path); + commands.push(format!("(cd {} && {})", subdir, cmd)); } } if root.join("pyproject.toml").exists() || root.join("requirements.txt").exists() { - commands.push("pytest"); + commands.push("pytest".to_string()); } if root.join("go.mod").exists() { - commands.push("go test ./..."); + commands.push("go test ./...".to_string()); } if commands.is_empty() { @@ -234,7 +270,7 @@ pub fn detect_script_test(root: &Path) -> 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_str(&cmd); script.push('\n'); } script @@ -1170,6 +1206,141 @@ mod tests { ); } + #[test] + fn detect_script_test_frontend_subdir_with_vitest_uses_npx_vitest() { + let dir = tempdir().unwrap(); + let frontend = dir.path().join("frontend"); + fs::create_dir_all(&frontend).unwrap(); + fs::write( + frontend.join("package.json"), + r#"{"devDependencies":{"vitest":"^1.0.0"},"scripts":{"test":"vitest run"}}"#, + ) + .unwrap(); + + let script = detect_script_test(dir.path()); + assert!( + script.contains("vitest run"), + "frontend with vitest should emit vitest run" + ); + assert!( + script.contains("cd frontend"), + "should cd into the frontend directory" + ); + assert!( + !script.contains("No tests configured"), + "should not use stub when frontend is detected" + ); + } + + #[test] + fn detect_script_test_frontend_subdir_with_jest_uses_npx_jest() { + let dir = tempdir().unwrap(); + let frontend = dir.path().join("frontend"); + fs::create_dir_all(&frontend).unwrap(); + fs::write( + frontend.join("package.json"), + r#"{"devDependencies":{"jest":"^29.0.0"},"scripts":{"test":"jest"}}"#, + ) + .unwrap(); + + let script = detect_script_test(dir.path()); + assert!( + script.contains("jest"), + "frontend with jest should emit jest" + ); + assert!( + script.contains("cd frontend"), + "should cd into the frontend directory" + ); + } + + #[test] + fn detect_script_test_frontend_subdir_no_known_runner_uses_npm_test() { + let dir = tempdir().unwrap(); + let frontend = dir.path().join("frontend"); + fs::create_dir_all(&frontend).unwrap(); + fs::write( + frontend.join("package.json"), + r#"{"scripts":{"test":"mocha"}}"#, + ) + .unwrap(); + + let script = detect_script_test(dir.path()); + assert!( + script.contains("npm test"), + "frontend without known runner should fall back to npm test" + ); + assert!(script.contains("cd frontend")); + } + + #[test] + fn detect_script_test_frontend_subdir_pnpm_uses_pnpm_vitest() { + let dir = tempdir().unwrap(); + let frontend = dir.path().join("frontend"); + fs::create_dir_all(&frontend).unwrap(); + fs::write( + frontend.join("package.json"), + r#"{"devDependencies":{"vitest":"^1.0.0"}}"#, + ) + .unwrap(); + fs::write(frontend.join("pnpm-lock.yaml"), "").unwrap(); + + let script = detect_script_test(dir.path()); + assert!( + script.contains("pnpm vitest run"), + "pnpm frontend with vitest should use pnpm vitest run" + ); + } + + #[test] + fn detect_script_test_rust_plus_frontend_subdir_both_included() { + let dir = tempdir().unwrap(); + fs::write( + dir.path().join("Cargo.toml"), + "[package]\nname = \"server\"\n", + ) + .unwrap(); + let frontend = dir.path().join("frontend"); + fs::create_dir_all(&frontend).unwrap(); + fs::write( + frontend.join("package.json"), + r#"{"devDependencies":{"vitest":"^1.0.0"}}"#, + ) + .unwrap(); + + let script = detect_script_test(dir.path()); + assert!( + script.contains("cargo test"), + "Rust + frontend should include cargo test" + ); + assert!( + script.contains("vitest run"), + "Rust + frontend should include vitest run" + ); + assert!( + script.contains("cd frontend"), + "Rust + frontend should cd into frontend" + ); + } + + #[test] + fn detect_script_test_client_subdir_detected() { + let dir = tempdir().unwrap(); + let client = dir.path().join("client"); + fs::create_dir_all(&client).unwrap(); + fs::write( + client.join("package.json"), + r#"{"scripts":{"test":"jest"}}"#, + ) + .unwrap(); + + let script = detect_script_test(dir.path()); + assert!( + script.contains("cd client"), + "client/ subdir should also be detected" + ); + } + #[test] fn detect_script_test_output_starts_with_shebang() { let dir = tempdir().unwrap();