From 4553d7215a80be26b931a8e3fd18521765ea8b45 Mon Sep 17 00:00:00 2001 From: dave Date: Wed, 15 Apr 2026 23:48:14 +0000 Subject: [PATCH] huskies: merge 586_bug_wizard_skips_context_and_stack_generation_when_files_already_exist_from_scaffold --- server/src/http/mcp/wizard_tools.rs | 51 +++++++++++++++++++++++++---- server/src/io/onboarding.rs | 2 +- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/server/src/http/mcp/wizard_tools.rs b/server/src/http/mcp/wizard_tools.rs index a92b74f5..779704a9 100644 --- a/server/src/http/mcp/wizard_tools.rs +++ b/server/src/http/mcp/wizard_tools.rs @@ -56,18 +56,27 @@ pub(crate) fn is_script_step(step: WizardStep) -> bool { ) } -/// Write `content` to `path` only when the file does not already exist. +/// Write `content` to `path`, skipping if the file already exists with real +/// (non-template) content. /// -/// Existing files (including `CLAUDE.md`) are never overwritten — the wizard -/// appends or skips per the acceptance criteria. For script steps the file is -/// also made executable after writing. +/// Scaffold template files (those containing [`TEMPLATE_SENTINEL`]) are treated +/// as placeholders and will be overwritten with the wizard-generated content. +/// Files with real user content are never overwritten. For script steps the +/// file is also made executable after writing. pub(crate) fn write_if_missing( path: &Path, content: &str, executable: bool, ) -> Result { + use crate::io::onboarding::TEMPLATE_SENTINEL; if path.exists() { - return Ok(false); // already present — skip silently + // Overwrite scaffold template placeholders; preserve real user content. + let is_template = std::fs::read_to_string(path) + .map(|s| s.contains(TEMPLATE_SENTINEL)) + .unwrap_or(false); + if !is_template { + return Ok(false); // real content already present — skip + } } if let Some(parent) = path.parent() { fs::create_dir_all(parent) @@ -473,13 +482,13 @@ mod tests { fn wizard_confirm_does_not_overwrite_existing_file() { let dir = TempDir::new().unwrap(); let ctx = setup(&dir); - // Pre-create the specs directory and file. + // Pre-create the specs directory and file with real (non-template) 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, "original content").unwrap(); - // Stage and confirm — existing file should NOT be overwritten. + // Stage and confirm — existing real file should NOT be overwritten. tool_wizard_generate(&serde_json::json!({"content": "new content"}), &ctx).unwrap(); let result = tool_wizard_confirm(&ctx).unwrap(); assert!(result.contains("already exists")); @@ -489,6 +498,34 @@ mod tests { ); } + #[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, + "\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." + ); + } + #[test] fn wizard_skip_advances_wizard() { let dir = TempDir::new().unwrap(); diff --git a/server/src/io/onboarding.rs b/server/src/io/onboarding.rs index d6d14d31..380977d9 100644 --- a/server/src/io/onboarding.rs +++ b/server/src/io/onboarding.rs @@ -5,7 +5,7 @@ use std::path::Path; /// Only untouched templates contain this marker — real project content /// will never include it, so it avoids false positives when the project /// itself is an "Agentic AI Code Assistant". -const TEMPLATE_SENTINEL: &str = ""; +pub(crate) const TEMPLATE_SENTINEL: &str = ""; /// Marker found in the default `script/test` scaffold output. const TEMPLATE_MARKER_SCRIPT: &str = "No tests configured";