2026-03-28 14:21:13 +00:00
|
|
|
//! MCP tool implementations for the interactive setup wizard.
|
|
|
|
|
//!
|
|
|
|
|
//! These tools allow Claude Code (and other MCP clients) to drive the setup
|
|
|
|
|
//! wizard entirely from the terminal without requiring the web UI or chat bot.
|
|
|
|
|
//!
|
|
|
|
|
//! Typical flow:
|
|
|
|
|
//! 1. `wizard_status` — inspect current state
|
|
|
|
|
//! 2. `wizard_generate` — read the codebase and call again with `content` to
|
|
|
|
|
//! stage generated text for review
|
|
|
|
|
//! 3. `wizard_confirm` — write staged content to disk and advance the wizard
|
|
|
|
|
//! 4. `wizard_skip` — skip a step that does not apply
|
|
|
|
|
//! 5. `wizard_retry` — discard staged content and regenerate from scratch
|
|
|
|
|
|
|
|
|
|
use crate::http::context::AppContext;
|
2026-04-24 16:41:58 +00:00
|
|
|
use crate::io::wizard::WizardStep;
|
|
|
|
|
use crate::service::wizard::state_machine;
|
|
|
|
|
use crate::service::wizard::{self as svc};
|
2026-03-28 14:21:13 +00:00
|
|
|
use serde_json::Value;
|
|
|
|
|
use std::path::Path;
|
|
|
|
|
|
2026-04-24 16:41:58 +00:00
|
|
|
// ── Thin adapters (kept for callers in chat/commands/setup.rs) ────────────────
|
2026-03-28 14:21:13 +00:00
|
|
|
|
2026-04-24 16:41:58 +00:00
|
|
|
/// Return the filesystem path for a step's output file.
|
2026-03-28 14:21:13 +00:00
|
|
|
///
|
2026-04-24 16:41:58 +00:00
|
|
|
/// Pure path concatenation — delegates to `service::wizard::state_machine`.
|
2026-04-13 14:07:08 +00:00
|
|
|
pub(crate) fn step_output_path(
|
|
|
|
|
project_root: &Path,
|
|
|
|
|
step: WizardStep,
|
|
|
|
|
) -> Option<std::path::PathBuf> {
|
2026-04-24 16:41:58 +00:00
|
|
|
state_machine::step_output_path(project_root, step)
|
2026-03-28 14:21:13 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-24 16:41:58 +00:00
|
|
|
/// Return true when `step` produces an executable script file.
|
2026-03-28 14:21:13 +00:00
|
|
|
pub(crate) fn is_script_step(step: WizardStep) -> bool {
|
2026-04-24 16:41:58 +00:00
|
|
|
state_machine::is_script_step(step)
|
2026-03-28 14:21:13 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-24 16:41:58 +00:00
|
|
|
/// Write `content` to `path`, skipping if the file already has real content.
|
2026-03-28 14:21:13 +00:00
|
|
|
///
|
2026-04-24 16:41:58 +00:00
|
|
|
/// Delegates to `service::wizard::write_step_file`.
|
2026-04-13 14:07:08 +00:00
|
|
|
pub(crate) fn write_if_missing(
|
|
|
|
|
path: &Path,
|
|
|
|
|
content: &str,
|
|
|
|
|
executable: bool,
|
|
|
|
|
) -> Result<bool, String> {
|
2026-04-24 16:41:58 +00:00
|
|
|
svc::write_step_file(path, content, executable).map_err(|e| e.to_string())
|
|
|
|
|
}
|
2026-03-28 14:21:13 +00:00
|
|
|
|
2026-04-24 16:41:58 +00:00
|
|
|
/// Return true when the project directory has no meaningful source files.
|
|
|
|
|
///
|
|
|
|
|
/// Delegates to `service::wizard::state_machine::is_bare_project` after
|
|
|
|
|
/// reading directory entries via `service::wizard::io`.
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
fn is_bare_project(project_root: &Path) -> bool {
|
|
|
|
|
use crate::service::wizard::io as wio;
|
|
|
|
|
let names = wio::list_dir_names(project_root);
|
|
|
|
|
state_machine::is_bare_project(&names)
|
2026-03-28 14:21:13 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-24 16:41:58 +00:00
|
|
|
/// Return a generation hint for `step` based on the project at `project_root`.
|
|
|
|
|
///
|
|
|
|
|
/// Reads filesystem state then delegates pure logic to `state_machine`.
|
|
|
|
|
pub(crate) fn generation_hint(step: WizardStep, project_root: &Path) -> String {
|
|
|
|
|
use crate::service::wizard::io as wio;
|
|
|
|
|
let names = wio::list_dir_names(project_root);
|
|
|
|
|
let tools = wio::detect_project_tools(project_root);
|
|
|
|
|
let is_bare = state_machine::is_bare_project(&names);
|
|
|
|
|
state_machine::generation_hint(step, is_bare, &tools)
|
2026-03-28 14:21:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── MCP tool handlers ─────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/// `wizard_status` — return current wizard state as a human-readable summary.
|
|
|
|
|
pub(super) fn tool_wizard_status(ctx: &AppContext) -> Result<String, String> {
|
|
|
|
|
let root = ctx.state.get_project_root()?;
|
2026-04-24 16:41:58 +00:00
|
|
|
svc::status(&root).map_err(|e| e.to_string())
|
2026-03-28 14:21:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// `wizard_generate` — mark the current step as generating or stage content.
|
|
|
|
|
///
|
|
|
|
|
/// Call with no `content` argument to mark the step as `Generating` and
|
|
|
|
|
/// receive a hint describing what to generate. Call again with a `content`
|
|
|
|
|
/// argument (the generated file body) to stage it for review; the step will
|
|
|
|
|
/// transition to `AwaitingConfirmation`. Content is **not** written to disk
|
|
|
|
|
/// until `wizard_confirm` is called.
|
|
|
|
|
pub(super) fn tool_wizard_generate(args: &Value, ctx: &AppContext) -> Result<String, String> {
|
|
|
|
|
let root = ctx.state.get_project_root()?;
|
2026-04-24 16:41:58 +00:00
|
|
|
let content = args.get("content").and_then(|v| v.as_str());
|
|
|
|
|
svc::generate(&root, content).map_err(|e| e.to_string())
|
2026-03-28 14:21:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// `wizard_confirm` — confirm the current step and write its content to disk.
|
|
|
|
|
///
|
|
|
|
|
/// If the step has staged content, the content is written to its target file
|
|
|
|
|
/// (only if that file does not already exist — existing files are never
|
|
|
|
|
/// overwritten). The step is then marked as `Confirmed` and the wizard
|
|
|
|
|
/// advances to the next pending step.
|
|
|
|
|
pub(super) fn tool_wizard_confirm(ctx: &AppContext) -> Result<String, String> {
|
|
|
|
|
let root = ctx.state.get_project_root()?;
|
2026-04-24 16:41:58 +00:00
|
|
|
svc::confirm(&root).map_err(|e| e.to_string())
|
2026-03-28 14:21:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// `wizard_skip` — skip the current step without writing any file.
|
|
|
|
|
pub(super) fn tool_wizard_skip(ctx: &AppContext) -> Result<String, String> {
|
|
|
|
|
let root = ctx.state.get_project_root()?;
|
2026-04-24 16:41:58 +00:00
|
|
|
svc::skip(&root).map_err(|e| e.to_string())
|
2026-03-28 14:21:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// `wizard_retry` — discard staged content and reset the current step to
|
|
|
|
|
/// `Pending` so it can be regenerated.
|
|
|
|
|
pub(super) fn tool_wizard_retry(ctx: &AppContext) -> Result<String, String> {
|
|
|
|
|
let root = ctx.state.get_project_root()?;
|
2026-04-24 16:41:58 +00:00
|
|
|
svc::retry(&root).map_err(|e| e.to_string())
|
2026-03-28 14:21:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── tests ─────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
use crate::http::context::AppContext;
|
2026-04-24 16:41:58 +00:00
|
|
|
use crate::io::wizard::{StepStatus, WizardState, format_wizard_state};
|
2026-03-28 14:21:13 +00:00
|
|
|
use tempfile::TempDir;
|
|
|
|
|
|
|
|
|
|
fn setup(dir: &TempDir) -> AppContext {
|
|
|
|
|
let root = dir.path().to_path_buf();
|
2026-04-03 16:12:52 +01:00
|
|
|
std::fs::create_dir_all(root.join(".huskies")).unwrap();
|
2026-03-28 14:21:13 +00:00
|
|
|
WizardState::init_if_missing(&root);
|
|
|
|
|
AppContext::new_test(root)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn wizard_status_returns_state() {
|
|
|
|
|
let dir = TempDir::new().unwrap();
|
|
|
|
|
let ctx = setup(&dir);
|
|
|
|
|
let result = tool_wizard_status(&ctx).unwrap();
|
|
|
|
|
assert!(result.contains("Setup wizard"));
|
|
|
|
|
assert!(result.contains("context"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn wizard_status_no_wizard_returns_error() {
|
|
|
|
|
let dir = TempDir::new().unwrap();
|
2026-04-03 16:12:52 +01:00
|
|
|
std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
|
2026-03-28 14:21:13 +00:00
|
|
|
let ctx = AppContext::new_test(dir.path().to_path_buf());
|
|
|
|
|
assert!(tool_wizard_status(&ctx).is_err());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn wizard_generate_marks_generating() {
|
|
|
|
|
let dir = TempDir::new().unwrap();
|
|
|
|
|
let ctx = setup(&dir);
|
|
|
|
|
let result = tool_wizard_generate(&serde_json::json!({}), &ctx).unwrap();
|
|
|
|
|
assert!(result.contains("generating"));
|
|
|
|
|
let state = WizardState::load(dir.path()).unwrap();
|
|
|
|
|
assert_eq!(state.steps[1].status, StepStatus::Generating);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn wizard_generate_with_content_stages_content() {
|
|
|
|
|
let dir = TempDir::new().unwrap();
|
|
|
|
|
let ctx = setup(&dir);
|
2026-04-13 14:07:08 +00:00
|
|
|
let result =
|
|
|
|
|
tool_wizard_generate(&serde_json::json!({"content": "# My Project"}), &ctx).unwrap();
|
2026-03-28 14:21:13 +00:00
|
|
|
assert!(result.contains("staged"));
|
|
|
|
|
let state = WizardState::load(dir.path()).unwrap();
|
|
|
|
|
assert_eq!(state.steps[1].status, StepStatus::AwaitingConfirmation);
|
|
|
|
|
assert_eq!(state.steps[1].content.as_deref(), Some("# My Project"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn wizard_confirm_writes_file_and_advances() {
|
|
|
|
|
let dir = TempDir::new().unwrap();
|
|
|
|
|
let ctx = setup(&dir);
|
|
|
|
|
// Stage content for Context step.
|
2026-04-13 14:07:08 +00:00
|
|
|
tool_wizard_generate(&serde_json::json!({"content": "# Context content"}), &ctx).unwrap();
|
2026-03-28 14:21:13 +00:00
|
|
|
let result = tool_wizard_confirm(&ctx).unwrap();
|
|
|
|
|
assert!(result.contains("confirmed"));
|
|
|
|
|
// File should now exist.
|
|
|
|
|
let context_path = dir
|
|
|
|
|
.path()
|
2026-04-03 16:12:52 +01:00
|
|
|
.join(".huskies")
|
2026-03-28 14:21:13 +00:00
|
|
|
.join("specs")
|
|
|
|
|
.join("00_CONTEXT.md");
|
|
|
|
|
assert!(context_path.exists());
|
|
|
|
|
assert_eq!(
|
|
|
|
|
std::fs::read_to_string(&context_path).unwrap(),
|
|
|
|
|
"# Context content"
|
|
|
|
|
);
|
|
|
|
|
// Wizard should have advanced.
|
|
|
|
|
let state = WizardState::load(dir.path()).unwrap();
|
|
|
|
|
assert_eq!(state.steps[1].status, StepStatus::Confirmed);
|
|
|
|
|
assert_eq!(state.current_step_index(), 2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn wizard_confirm_does_not_overwrite_existing_file() {
|
|
|
|
|
let dir = TempDir::new().unwrap();
|
|
|
|
|
let ctx = setup(&dir);
|
2026-04-15 23:48:14 +00:00
|
|
|
// Pre-create the specs directory and file with real (non-template) content.
|
2026-04-03 16:12:52 +01:00
|
|
|
let specs_dir = dir.path().join(".huskies").join("specs");
|
2026-03-28 14:21:13 +00:00
|
|
|
std::fs::create_dir_all(&specs_dir).unwrap();
|
|
|
|
|
let context_path = specs_dir.join("00_CONTEXT.md");
|
|
|
|
|
std::fs::write(&context_path, "original content").unwrap();
|
|
|
|
|
|
2026-04-15 23:48:14 +00:00
|
|
|
// Stage and confirm — existing real file should NOT be overwritten.
|
2026-04-13 14:07:08 +00:00
|
|
|
tool_wizard_generate(&serde_json::json!({"content": "new content"}), &ctx).unwrap();
|
2026-03-28 14:21:13 +00:00
|
|
|
let result = tool_wizard_confirm(&ctx).unwrap();
|
|
|
|
|
assert!(result.contains("already exists"));
|
|
|
|
|
assert_eq!(
|
|
|
|
|
std::fs::read_to_string(&context_path).unwrap(),
|
|
|
|
|
"original content"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 23:48:14 +00:00
|
|
|
#[test]
|
|
|
|
|
fn wizard_confirm_overwrites_scaffold_template_file() {
|
|
|
|
|
let dir = TempDir::new().unwrap();
|
|
|
|
|
let ctx = setup(&dir);
|
|
|
|
|
// Pre-create the file with scaffold template placeholder content.
|
|
|
|
|
let specs_dir = dir.path().join(".huskies").join("specs");
|
|
|
|
|
std::fs::create_dir_all(&specs_dir).unwrap();
|
|
|
|
|
let context_path = specs_dir.join("00_CONTEXT.md");
|
|
|
|
|
std::fs::write(
|
|
|
|
|
&context_path,
|
|
|
|
|
"<!-- huskies:scaffold-template -->\n# Project Context\n\nTODO: Describe...",
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
// Stage and confirm — template placeholder should be overwritten with generated content.
|
|
|
|
|
tool_wizard_generate(
|
|
|
|
|
&serde_json::json!({"content": "# My Real Project\n\nThis is a real project."}),
|
|
|
|
|
&ctx,
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
let result = tool_wizard_confirm(&ctx).unwrap();
|
|
|
|
|
assert!(result.contains("confirmed"));
|
|
|
|
|
assert_eq!(
|
|
|
|
|
std::fs::read_to_string(&context_path).unwrap(),
|
|
|
|
|
"# My Real Project\n\nThis is a real project."
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-28 14:21:13 +00:00
|
|
|
#[test]
|
|
|
|
|
fn wizard_skip_advances_wizard() {
|
|
|
|
|
let dir = TempDir::new().unwrap();
|
|
|
|
|
let ctx = setup(&dir);
|
|
|
|
|
let result = tool_wizard_skip(&ctx).unwrap();
|
|
|
|
|
assert!(result.contains("skipped"));
|
|
|
|
|
let state = WizardState::load(dir.path()).unwrap();
|
|
|
|
|
assert_eq!(state.steps[1].status, StepStatus::Skipped);
|
|
|
|
|
assert_eq!(state.current_step_index(), 2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn wizard_retry_resets_to_pending() {
|
|
|
|
|
let dir = TempDir::new().unwrap();
|
|
|
|
|
let ctx = setup(&dir);
|
|
|
|
|
// Stage content first.
|
2026-04-13 14:07:08 +00:00
|
|
|
tool_wizard_generate(&serde_json::json!({"content": "some content"}), &ctx).unwrap();
|
2026-03-28 14:21:13 +00:00
|
|
|
let result = tool_wizard_retry(&ctx).unwrap();
|
|
|
|
|
assert!(result.contains("reset"));
|
|
|
|
|
let state = WizardState::load(dir.path()).unwrap();
|
|
|
|
|
assert_eq!(state.steps[1].status, StepStatus::Pending);
|
|
|
|
|
assert!(state.steps[1].content.is_none());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn wizard_complete_returns_done_message() {
|
|
|
|
|
let dir = TempDir::new().unwrap();
|
|
|
|
|
let ctx = setup(&dir);
|
2026-04-16 00:18:42 +00:00
|
|
|
// Skip all remaining steps (scaffold is pre-confirmed, so 7 remaining).
|
|
|
|
|
for _ in 0..7 {
|
2026-03-28 14:21:13 +00:00
|
|
|
tool_wizard_skip(&ctx).unwrap();
|
|
|
|
|
}
|
|
|
|
|
let result = tool_wizard_status(&ctx).unwrap();
|
|
|
|
|
assert!(result.contains("complete"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn format_wizard_state_shows_all_steps() {
|
|
|
|
|
let mut state = WizardState::default();
|
|
|
|
|
state.steps[0].status = StepStatus::Confirmed;
|
|
|
|
|
let output = format_wizard_state(&state);
|
|
|
|
|
assert!(output.contains("✓"));
|
|
|
|
|
assert!(output.contains("Scaffold"));
|
|
|
|
|
assert!(output.contains("← current"));
|
|
|
|
|
}
|
2026-03-29 00:42:57 +00:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn is_bare_project_detects_empty_dir() {
|
|
|
|
|
let dir = TempDir::new().unwrap();
|
|
|
|
|
assert!(is_bare_project(dir.path()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn is_bare_project_detects_scaffold_only_dir() {
|
|
|
|
|
let dir = TempDir::new().unwrap();
|
2026-04-03 16:12:52 +01:00
|
|
|
std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
|
2026-03-29 00:42:57 +00:00
|
|
|
std::fs::write(dir.path().join("CLAUDE.md"), "# Claude").unwrap();
|
|
|
|
|
std::fs::write(dir.path().join("README.md"), "# Readme").unwrap();
|
|
|
|
|
std::fs::create_dir_all(dir.path().join("script")).unwrap();
|
|
|
|
|
assert!(is_bare_project(dir.path()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn is_bare_project_false_when_source_files_exist() {
|
|
|
|
|
let dir = TempDir::new().unwrap();
|
2026-04-03 16:12:52 +01:00
|
|
|
std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
|
2026-03-29 00:42:57 +00:00
|
|
|
std::fs::write(dir.path().join("Cargo.toml"), "[package]").unwrap();
|
|
|
|
|
assert!(!is_bare_project(dir.path()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn is_bare_project_false_with_src_directory() {
|
|
|
|
|
let dir = TempDir::new().unwrap();
|
|
|
|
|
std::fs::create_dir_all(dir.path().join("src")).unwrap();
|
|
|
|
|
assert!(!is_bare_project(dir.path()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn generation_hint_bare_context_asks_user() {
|
|
|
|
|
let dir = TempDir::new().unwrap();
|
|
|
|
|
// Bare project — only scaffolding
|
2026-04-03 16:12:52 +01:00
|
|
|
std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
|
2026-03-29 00:42:57 +00:00
|
|
|
let hint = generation_hint(WizardStep::Context, dir.path());
|
|
|
|
|
assert!(hint.contains("bare project"));
|
|
|
|
|
assert!(hint.contains("Ask the user"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn generation_hint_bare_stack_asks_user() {
|
|
|
|
|
let dir = TempDir::new().unwrap();
|
2026-04-03 16:12:52 +01:00
|
|
|
std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
|
2026-03-29 00:42:57 +00:00
|
|
|
let hint = generation_hint(WizardStep::Stack, dir.path());
|
|
|
|
|
assert!(hint.contains("bare project"));
|
|
|
|
|
assert!(hint.contains("Ask the user"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn generation_hint_bare_test_script_references_stack() {
|
|
|
|
|
let dir = TempDir::new().unwrap();
|
2026-04-03 16:12:52 +01:00
|
|
|
std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
|
2026-03-29 00:42:57 +00:00
|
|
|
let hint = generation_hint(WizardStep::TestScript, dir.path());
|
|
|
|
|
assert!(hint.contains("bare project"));
|
|
|
|
|
assert!(hint.contains("STACK.md"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn generation_hint_bare_release_script_references_stack() {
|
|
|
|
|
let dir = TempDir::new().unwrap();
|
2026-04-03 16:12:52 +01:00
|
|
|
std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
|
2026-03-29 00:42:57 +00:00
|
|
|
let hint = generation_hint(WizardStep::ReleaseScript, dir.path());
|
|
|
|
|
assert!(hint.contains("bare project"));
|
|
|
|
|
assert!(hint.contains("STACK.md"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn generation_hint_bare_test_coverage_references_stack() {
|
|
|
|
|
let dir = TempDir::new().unwrap();
|
2026-04-03 16:12:52 +01:00
|
|
|
std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
|
2026-03-29 00:42:57 +00:00
|
|
|
let hint = generation_hint(WizardStep::TestCoverage, dir.path());
|
|
|
|
|
assert!(hint.contains("bare project"));
|
|
|
|
|
assert!(hint.contains("STACK.md"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn generation_hint_existing_project_reads_code() {
|
|
|
|
|
let dir = TempDir::new().unwrap();
|
|
|
|
|
std::fs::write(dir.path().join("Cargo.toml"), "[package]").unwrap();
|
|
|
|
|
let hint = generation_hint(WizardStep::Context, dir.path());
|
|
|
|
|
assert!(hint.contains("Read the project"));
|
|
|
|
|
assert!(!hint.contains("bare project"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn generation_hint_existing_project_test_script_detects_cargo() {
|
|
|
|
|
let dir = TempDir::new().unwrap();
|
|
|
|
|
std::fs::write(dir.path().join("Cargo.toml"), "[package]").unwrap();
|
|
|
|
|
let hint = generation_hint(WizardStep::TestScript, dir.path());
|
|
|
|
|
assert!(hint.contains("cargo nextest"));
|
|
|
|
|
assert!(!hint.contains("bare project"));
|
|
|
|
|
}
|
2026-04-16 00:18:42 +00:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn generation_hint_bare_build_script_references_stack() {
|
|
|
|
|
let dir = TempDir::new().unwrap();
|
|
|
|
|
std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
|
|
|
|
|
let hint = generation_hint(WizardStep::BuildScript, dir.path());
|
|
|
|
|
assert!(hint.contains("bare project"));
|
|
|
|
|
assert!(hint.contains("STACK.md"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn generation_hint_bare_lint_script_references_stack() {
|
|
|
|
|
let dir = TempDir::new().unwrap();
|
|
|
|
|
std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
|
|
|
|
|
let hint = generation_hint(WizardStep::LintScript, dir.path());
|
|
|
|
|
assert!(hint.contains("bare project"));
|
|
|
|
|
assert!(hint.contains("STACK.md"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn generation_hint_existing_project_build_script_detects_cargo() {
|
|
|
|
|
let dir = TempDir::new().unwrap();
|
|
|
|
|
std::fs::write(dir.path().join("Cargo.toml"), "[package]").unwrap();
|
|
|
|
|
let hint = generation_hint(WizardStep::BuildScript, dir.path());
|
|
|
|
|
assert!(hint.contains("cargo build --release"));
|
|
|
|
|
assert!(!hint.contains("bare project"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn generation_hint_existing_project_lint_script_detects_cargo() {
|
|
|
|
|
let dir = TempDir::new().unwrap();
|
|
|
|
|
std::fs::write(dir.path().join("Cargo.toml"), "[package]").unwrap();
|
|
|
|
|
let hint = generation_hint(WizardStep::LintScript, dir.path());
|
|
|
|
|
assert!(hint.contains("cargo fmt --all --check"));
|
|
|
|
|
assert!(hint.contains("cargo clippy -- -D warnings"));
|
|
|
|
|
assert!(!hint.contains("bare project"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn step_output_path_build_script_returns_script_build() {
|
|
|
|
|
let dir = TempDir::new().unwrap();
|
|
|
|
|
let path = step_output_path(dir.path(), WizardStep::BuildScript).unwrap();
|
|
|
|
|
assert!(path.ends_with("script/build"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn step_output_path_lint_script_returns_script_lint() {
|
|
|
|
|
let dir = TempDir::new().unwrap();
|
|
|
|
|
let path = step_output_path(dir.path(), WizardStep::LintScript).unwrap();
|
|
|
|
|
assert!(path.ends_with("script/lint"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn is_script_step_includes_build_and_lint() {
|
|
|
|
|
assert!(is_script_step(WizardStep::BuildScript));
|
|
|
|
|
assert!(is_script_step(WizardStep::LintScript));
|
|
|
|
|
}
|
2026-03-28 14:21:13 +00:00
|
|
|
}
|