huskies: merge 846
This commit is contained in:
@@ -142,596 +142,4 @@ pub(crate) fn scaffold_story_kit(root: &Path, port: u16) -> Result<(), String> {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn scaffold_story_kit_creates_structure() {
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
assert!(dir.path().join(".huskies/README.md").exists());
|
||||
assert!(dir.path().join(".huskies/project.toml").exists());
|
||||
assert!(dir.path().join(".huskies/agents.toml").exists());
|
||||
assert!(dir.path().join(".huskies/specs/00_CONTEXT.md").exists());
|
||||
assert!(dir.path().join(".huskies/specs/tech/STACK.md").exists());
|
||||
// Old stories/ dirs should NOT be created
|
||||
assert!(!dir.path().join(".huskies/stories").exists());
|
||||
assert!(dir.path().join("script/test").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_story_kit_does_not_create_work_pipeline_dirs() {
|
||||
// After 763 the `.huskies/work/` tree is no longer scaffolded; CRDT is
|
||||
// the canonical pipeline state store.
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
let work_root = dir.path().join(".huskies/work");
|
||||
assert!(
|
||||
!work_root.exists(),
|
||||
".huskies/work/ should not be created at scaffold time"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_story_kit_agents_toml_has_coder_qa_mergemaster() {
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
// Agent definitions go into agents.toml, not project.toml.
|
||||
let agents = fs::read_to_string(dir.path().join(".huskies/agents.toml")).unwrap();
|
||||
assert!(agents.contains("[[agent]]"));
|
||||
assert!(agents.contains("stage = \"coder\""));
|
||||
assert!(agents.contains("stage = \"qa\""));
|
||||
assert!(agents.contains("stage = \"mergemaster\""));
|
||||
assert!(agents.contains("model = \"sonnet\""));
|
||||
|
||||
// project.toml should NOT contain [[agent]] blocks.
|
||||
let project = fs::read_to_string(dir.path().join(".huskies/project.toml")).unwrap();
|
||||
assert!(!project.contains("[[agent]]"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_project_toml_contains_rate_limit_and_timezone_comments() {
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
let content = fs::read_to_string(dir.path().join(".huskies/project.toml")).unwrap();
|
||||
assert!(
|
||||
content.contains("rate_limit_notifications"),
|
||||
"project.toml scaffold should document rate_limit_notifications"
|
||||
);
|
||||
assert!(
|
||||
content.contains("timezone"),
|
||||
"project.toml scaffold should document timezone"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_project_toml_contains_max_retries_with_default_value() {
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
let content = fs::read_to_string(dir.path().join(".huskies/project.toml")).unwrap();
|
||||
assert!(
|
||||
content.contains("max_retries = 2"),
|
||||
"project.toml scaffold should include max_retries with default value 2"
|
||||
);
|
||||
assert!(
|
||||
content.contains("Maximum number of retries"),
|
||||
"project.toml scaffold should include a comment explaining max_retries"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_project_toml_contains_commented_out_optional_fields() {
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
let content = fs::read_to_string(dir.path().join(".huskies/project.toml")).unwrap();
|
||||
assert!(
|
||||
content.contains("# default_coder_model"),
|
||||
"project.toml scaffold should include commented-out default_coder_model"
|
||||
);
|
||||
assert!(
|
||||
content.contains("# max_coders"),
|
||||
"project.toml scaffold should include commented-out max_coders"
|
||||
);
|
||||
assert!(
|
||||
content.contains("# base_branch"),
|
||||
"project.toml scaffold should include commented-out base_branch"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_project_toml_round_trips_through_project_config_load() {
|
||||
use crate::config::ProjectConfig;
|
||||
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
// The generated project.toml must parse without error.
|
||||
let config = ProjectConfig::load(dir.path())
|
||||
.expect("Generated project.toml should parse without error");
|
||||
|
||||
// Key defaults must survive the round-trip.
|
||||
assert_eq!(config.default_qa, "server");
|
||||
assert_eq!(config.max_retries, 2);
|
||||
assert!(
|
||||
config.rate_limit_notifications,
|
||||
"rate_limit_notifications should default to true"
|
||||
);
|
||||
assert!(
|
||||
config.default_coder_model.is_none(),
|
||||
"default_coder_model should be None when commented out"
|
||||
);
|
||||
assert!(
|
||||
config.max_coders.is_none(),
|
||||
"max_coders should be None when commented out"
|
||||
);
|
||||
assert!(
|
||||
config.base_branch.is_none(),
|
||||
"base_branch should be None when commented out"
|
||||
);
|
||||
assert!(
|
||||
config.timezone.is_none(),
|
||||
"timezone should be None when commented out"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_context_is_blank_template_not_story_kit_content() {
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
let content = fs::read_to_string(dir.path().join(".huskies/specs/00_CONTEXT.md")).unwrap();
|
||||
assert!(content.contains("<!-- huskies:scaffold-template -->"));
|
||||
assert!(content.contains("## High-Level Goal"));
|
||||
assert!(content.contains("## Core Features"));
|
||||
assert!(content.contains("## Domain Definition"));
|
||||
assert!(content.contains("## Glossary"));
|
||||
// Must NOT contain Story Kit-specific content
|
||||
assert!(!content.contains("Agentic AI Code Assistant"));
|
||||
assert!(!content.contains("Poem HTTP server"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_stack_is_blank_template_not_story_kit_content() {
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
let content = fs::read_to_string(dir.path().join(".huskies/specs/tech/STACK.md")).unwrap();
|
||||
assert!(content.contains("<!-- huskies:scaffold-template -->"));
|
||||
assert!(content.contains("## Core Stack"));
|
||||
assert!(content.contains("## Coding Standards"));
|
||||
assert!(content.contains("## Quality Gates"));
|
||||
assert!(content.contains("## Libraries"));
|
||||
// Must NOT contain Story Kit-specific content
|
||||
assert!(!content.contains("Poem HTTP server"));
|
||||
assert!(!content.contains("TypeScript + React"));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn scaffold_story_kit_creates_executable_script_test() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
let script_test = dir.path().join("script/test");
|
||||
assert!(script_test.exists(), "script/test should be created");
|
||||
let perms = fs::metadata(&script_test).unwrap().permissions();
|
||||
assert!(
|
||||
perms.mode() & 0o111 != 0,
|
||||
"script/test should be executable"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_story_kit_does_not_overwrite_existing() {
|
||||
let dir = tempdir().unwrap();
|
||||
let readme = dir.path().join(".huskies/README.md");
|
||||
fs::create_dir_all(readme.parent().unwrap()).unwrap();
|
||||
fs::write(&readme, "custom content").unwrap();
|
||||
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
assert_eq!(fs::read_to_string(&readme).unwrap(), "custom content");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_story_kit_is_idempotent() {
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
let readme_content = fs::read_to_string(dir.path().join(".huskies/README.md")).unwrap();
|
||||
let toml_content = fs::read_to_string(dir.path().join(".huskies/project.toml")).unwrap();
|
||||
|
||||
// Run again — must not change content or add duplicate .gitignore entries
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
fs::read_to_string(dir.path().join(".huskies/README.md")).unwrap(),
|
||||
readme_content
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read_to_string(dir.path().join(".huskies/project.toml")).unwrap(),
|
||||
toml_content
|
||||
);
|
||||
|
||||
let story_kit_gitignore =
|
||||
fs::read_to_string(dir.path().join(".huskies/.gitignore")).unwrap();
|
||||
let count = story_kit_gitignore
|
||||
.lines()
|
||||
.filter(|l| l.trim() == "worktrees/")
|
||||
.count();
|
||||
assert_eq!(
|
||||
count, 1,
|
||||
".huskies/.gitignore should not have duplicate entries"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_story_kit_existing_git_repo_no_commit() {
|
||||
let dir = tempdir().unwrap();
|
||||
|
||||
// Initialize a git repo before scaffold
|
||||
std::process::Command::new("git")
|
||||
.args(["init"])
|
||||
.current_dir(dir.path())
|
||||
.status()
|
||||
.unwrap();
|
||||
std::process::Command::new("git")
|
||||
.args([
|
||||
"-c",
|
||||
"user.email=test@test.com",
|
||||
"-c",
|
||||
"user.name=Test",
|
||||
"commit",
|
||||
"--allow-empty",
|
||||
"-m",
|
||||
"pre-scaffold",
|
||||
])
|
||||
.current_dir(dir.path())
|
||||
.status()
|
||||
.unwrap();
|
||||
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
// Only 1 commit should exist — scaffold must not commit into an existing repo
|
||||
let log_output = std::process::Command::new("git")
|
||||
.args(["log", "--oneline"])
|
||||
.current_dir(dir.path())
|
||||
.output()
|
||||
.unwrap();
|
||||
let log = String::from_utf8_lossy(&log_output.stdout);
|
||||
let commit_count = log.lines().count();
|
||||
assert_eq!(
|
||||
commit_count, 1,
|
||||
"scaffold should not create a commit in an existing git repo"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_creates_story_kit_gitignore_with_relative_entries() {
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
// .huskies/.gitignore must contain relative patterns for files under .huskies/
|
||||
let sk_content = fs::read_to_string(dir.path().join(".huskies/.gitignore")).unwrap();
|
||||
assert!(sk_content.contains("worktrees/"));
|
||||
assert!(sk_content.contains("merge_workspace/"));
|
||||
assert!(sk_content.contains("coverage/"));
|
||||
assert!(sk_content.contains("matrix_history.json"));
|
||||
assert!(sk_content.contains("timers.json"));
|
||||
// Must NOT contain absolute .huskies/ prefixed paths
|
||||
assert!(!sk_content.contains(".huskies/"));
|
||||
|
||||
// Root .gitignore must contain root-level huskies entries
|
||||
let root_content = fs::read_to_string(dir.path().join(".gitignore")).unwrap();
|
||||
assert!(root_content.contains(".huskies_port"));
|
||||
// store.json now lives inside .huskies/ and must NOT appear in root .gitignore
|
||||
assert!(!root_content.contains("store.json"));
|
||||
// Root .gitignore must NOT contain .huskies/ sub-directory patterns
|
||||
assert!(!root_content.contains(".huskies/worktrees/"));
|
||||
assert!(!root_content.contains(".huskies/merge_workspace/"));
|
||||
assert!(!root_content.contains(".huskies/coverage/"));
|
||||
// store.json must be in .huskies/.gitignore instead
|
||||
assert!(sk_content.contains("store.json"));
|
||||
// Database files must be ignored so novice users don't accidentally commit them
|
||||
assert!(sk_content.contains("pipeline.db"));
|
||||
assert!(sk_content.contains("*.db"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_story_kit_gitignore_does_not_duplicate_existing_entries() {
|
||||
let dir = tempdir().unwrap();
|
||||
// Pre-create .huskies dir and .gitignore with some entries already present
|
||||
fs::create_dir_all(dir.path().join(".huskies")).unwrap();
|
||||
fs::write(
|
||||
dir.path().join(".huskies/.gitignore"),
|
||||
"worktrees/\ncoverage/\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
let content = fs::read_to_string(dir.path().join(".huskies/.gitignore")).unwrap();
|
||||
let worktrees_count = content.lines().filter(|l| l.trim() == "worktrees/").count();
|
||||
assert_eq!(worktrees_count, 1, "worktrees/ should not be duplicated");
|
||||
let coverage_count = content.lines().filter(|l| l.trim() == "coverage/").count();
|
||||
assert_eq!(coverage_count, 1, "coverage/ should not be duplicated");
|
||||
// The missing entry must have been added
|
||||
assert!(content.contains("merge_workspace/"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_creates_claude_md_at_project_root() {
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
let claude_md = dir.path().join("CLAUDE.md");
|
||||
assert!(
|
||||
claude_md.exists(),
|
||||
"CLAUDE.md should be created at project root"
|
||||
);
|
||||
|
||||
let content = fs::read_to_string(&claude_md).unwrap();
|
||||
assert!(
|
||||
content.contains("<!-- huskies:scaffold-template -->"),
|
||||
"CLAUDE.md should contain the scaffold sentinel"
|
||||
);
|
||||
assert!(
|
||||
content.contains("Read .huskies/README.md"),
|
||||
"CLAUDE.md should include directive to read .huskies/README.md"
|
||||
);
|
||||
assert!(
|
||||
content.contains("Never chain shell commands"),
|
||||
"CLAUDE.md should include command chaining rule"
|
||||
);
|
||||
assert!(
|
||||
content.contains("wizard_status"),
|
||||
"CLAUDE.md should instruct Claude to call wizard_status on first conversation"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_does_not_overwrite_existing_claude_md() {
|
||||
let dir = tempdir().unwrap();
|
||||
let claude_md = dir.path().join("CLAUDE.md");
|
||||
fs::write(&claude_md, "custom CLAUDE.md content").unwrap();
|
||||
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
fs::read_to_string(&claude_md).unwrap(),
|
||||
"custom CLAUDE.md content",
|
||||
"scaffold should not overwrite an existing CLAUDE.md"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_story_kit_writes_mcp_json_with_port() {
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 4242).unwrap();
|
||||
|
||||
let mcp_path = dir.path().join(".mcp.json");
|
||||
assert!(mcp_path.exists(), ".mcp.json should be created by scaffold");
|
||||
let content = fs::read_to_string(&mcp_path).unwrap();
|
||||
assert!(
|
||||
content.contains("4242"),
|
||||
".mcp.json should reference the given port"
|
||||
);
|
||||
assert!(
|
||||
content.contains("localhost"),
|
||||
".mcp.json should reference localhost"
|
||||
);
|
||||
assert!(
|
||||
content.contains("huskies"),
|
||||
".mcp.json should name the huskies server"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_story_kit_does_not_overwrite_existing_mcp_json() {
|
||||
let dir = tempdir().unwrap();
|
||||
let mcp_path = dir.path().join(".mcp.json");
|
||||
fs::write(&mcp_path, "{\"custom\": true}").unwrap();
|
||||
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
fs::read_to_string(&mcp_path).unwrap(),
|
||||
"{\"custom\": true}",
|
||||
"scaffold should not overwrite an existing .mcp.json"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_gitignore_includes_mcp_json() {
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
let root_gitignore = fs::read_to_string(dir.path().join(".gitignore")).unwrap();
|
||||
assert!(
|
||||
root_gitignore.contains(".mcp.json"),
|
||||
"root .gitignore should include .mcp.json (port is environment-specific)"
|
||||
);
|
||||
}
|
||||
|
||||
#[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"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_story_kit_creates_script_build_and_lint() {
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
assert!(
|
||||
dir.path().join("script/build").exists(),
|
||||
"script/build should be created by scaffold"
|
||||
);
|
||||
assert!(
|
||||
dir.path().join("script/lint").exists(),
|
||||
"script/lint should be created by scaffold"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn scaffold_story_kit_creates_executable_script_build_and_lint() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
for name in &["build", "lint"] {
|
||||
let path = dir.path().join("script").join(name);
|
||||
assert!(path.exists(), "script/{name} should be created");
|
||||
let perms = fs::metadata(&path).unwrap().permissions();
|
||||
assert!(
|
||||
perms.mode() & 0o111 != 0,
|
||||
"script/{name} should be executable"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_script_build_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/build")).unwrap();
|
||||
assert!(
|
||||
content.contains("cargo build --release"),
|
||||
"Rust project scaffold should set cargo build --release in script/build"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_script_lint_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/lint")).unwrap();
|
||||
assert!(
|
||||
content.contains("cargo fmt --all --check"),
|
||||
"Rust project scaffold should include fmt check in script/lint"
|
||||
);
|
||||
assert!(
|
||||
content.contains("cargo clippy -- -D warnings"),
|
||||
"Rust project scaffold should include clippy in script/lint"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_project_toml_contains_detected_components() {
|
||||
let dir = tempdir().unwrap();
|
||||
// Place a Cargo.toml in the project root before scaffolding
|
||||
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(".huskies/project.toml")).unwrap();
|
||||
assert!(
|
||||
content.contains("[[component]]"),
|
||||
"project.toml should contain a component entry"
|
||||
);
|
||||
assert!(
|
||||
content.contains("name = \"server\""),
|
||||
"Rust project should have a 'server' component"
|
||||
);
|
||||
assert!(
|
||||
content.contains("cargo check"),
|
||||
"Rust component should have cargo check setup"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_project_toml_fallback_when_no_stack_detected() {
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
let content = fs::read_to_string(dir.path().join(".huskies/project.toml")).unwrap();
|
||||
assert!(
|
||||
content.contains("[[component]]"),
|
||||
"project.toml should always have at least one component"
|
||||
);
|
||||
// Fallback uses generic app component with empty setup — no Rust-specific commands
|
||||
assert!(
|
||||
content.contains("name = \"app\""),
|
||||
"fallback should use generic 'app' component name"
|
||||
);
|
||||
assert!(
|
||||
!content.contains("cargo"),
|
||||
"fallback must not contain Rust-specific commands for non-Rust projects"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_does_not_overwrite_existing_project_toml_with_components() {
|
||||
let dir = tempdir().unwrap();
|
||||
let sk_dir = dir.path().join(".huskies");
|
||||
fs::create_dir_all(&sk_dir).unwrap();
|
||||
let existing = "[[component]]\nname = \"custom\"\npath = \".\"\nsetup = [\"make build\"]\n";
|
||||
fs::write(sk_dir.join("project.toml"), existing).unwrap();
|
||||
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
let content = fs::read_to_string(sk_dir.join("project.toml")).unwrap();
|
||||
assert_eq!(
|
||||
content, existing,
|
||||
"scaffold should not overwrite existing project.toml"
|
||||
);
|
||||
}
|
||||
}
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,594 @@
|
||||
//! Integration tests for the scaffold module — verifies that `scaffold_story_kit`
|
||||
//! creates the correct directory structure, files, and git state.
|
||||
|
||||
use super::*;
|
||||
use std::fs;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn scaffold_story_kit_creates_structure() {
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
assert!(dir.path().join(".huskies/README.md").exists());
|
||||
assert!(dir.path().join(".huskies/project.toml").exists());
|
||||
assert!(dir.path().join(".huskies/agents.toml").exists());
|
||||
assert!(dir.path().join(".huskies/specs/00_CONTEXT.md").exists());
|
||||
assert!(dir.path().join(".huskies/specs/tech/STACK.md").exists());
|
||||
// Old stories/ dirs should NOT be created
|
||||
assert!(!dir.path().join(".huskies/stories").exists());
|
||||
assert!(dir.path().join("script/test").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_story_kit_does_not_create_work_pipeline_dirs() {
|
||||
// After 763 the `.huskies/work/` tree is no longer scaffolded; CRDT is
|
||||
// the canonical pipeline state store.
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
let work_root = dir.path().join(".huskies/work");
|
||||
assert!(
|
||||
!work_root.exists(),
|
||||
".huskies/work/ should not be created at scaffold time"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_story_kit_agents_toml_has_coder_qa_mergemaster() {
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
// Agent definitions go into agents.toml, not project.toml.
|
||||
let agents = fs::read_to_string(dir.path().join(".huskies/agents.toml")).unwrap();
|
||||
assert!(agents.contains("[[agent]]"));
|
||||
assert!(agents.contains("stage = \"coder\""));
|
||||
assert!(agents.contains("stage = \"qa\""));
|
||||
assert!(agents.contains("stage = \"mergemaster\""));
|
||||
assert!(agents.contains("model = \"sonnet\""));
|
||||
|
||||
// project.toml should NOT contain [[agent]] blocks.
|
||||
let project = fs::read_to_string(dir.path().join(".huskies/project.toml")).unwrap();
|
||||
assert!(!project.contains("[[agent]]"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_project_toml_contains_rate_limit_and_timezone_comments() {
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
let content = fs::read_to_string(dir.path().join(".huskies/project.toml")).unwrap();
|
||||
assert!(
|
||||
content.contains("rate_limit_notifications"),
|
||||
"project.toml scaffold should document rate_limit_notifications"
|
||||
);
|
||||
assert!(
|
||||
content.contains("timezone"),
|
||||
"project.toml scaffold should document timezone"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_project_toml_contains_max_retries_with_default_value() {
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
let content = fs::read_to_string(dir.path().join(".huskies/project.toml")).unwrap();
|
||||
assert!(
|
||||
content.contains("max_retries = 2"),
|
||||
"project.toml scaffold should include max_retries with default value 2"
|
||||
);
|
||||
assert!(
|
||||
content.contains("Maximum number of retries"),
|
||||
"project.toml scaffold should include a comment explaining max_retries"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_project_toml_contains_commented_out_optional_fields() {
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
let content = fs::read_to_string(dir.path().join(".huskies/project.toml")).unwrap();
|
||||
assert!(
|
||||
content.contains("# default_coder_model"),
|
||||
"project.toml scaffold should include commented-out default_coder_model"
|
||||
);
|
||||
assert!(
|
||||
content.contains("# max_coders"),
|
||||
"project.toml scaffold should include commented-out max_coders"
|
||||
);
|
||||
assert!(
|
||||
content.contains("# base_branch"),
|
||||
"project.toml scaffold should include commented-out base_branch"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_project_toml_round_trips_through_project_config_load() {
|
||||
use crate::config::ProjectConfig;
|
||||
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
// The generated project.toml must parse without error.
|
||||
let config =
|
||||
ProjectConfig::load(dir.path()).expect("Generated project.toml should parse without error");
|
||||
|
||||
// Key defaults must survive the round-trip.
|
||||
assert_eq!(config.default_qa, "server");
|
||||
assert_eq!(config.max_retries, 2);
|
||||
assert!(
|
||||
config.rate_limit_notifications,
|
||||
"rate_limit_notifications should default to true"
|
||||
);
|
||||
assert!(
|
||||
config.default_coder_model.is_none(),
|
||||
"default_coder_model should be None when commented out"
|
||||
);
|
||||
assert!(
|
||||
config.max_coders.is_none(),
|
||||
"max_coders should be None when commented out"
|
||||
);
|
||||
assert!(
|
||||
config.base_branch.is_none(),
|
||||
"base_branch should be None when commented out"
|
||||
);
|
||||
assert!(
|
||||
config.timezone.is_none(),
|
||||
"timezone should be None when commented out"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_context_is_blank_template_not_story_kit_content() {
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
let content = fs::read_to_string(dir.path().join(".huskies/specs/00_CONTEXT.md")).unwrap();
|
||||
assert!(content.contains("<!-- huskies:scaffold-template -->"));
|
||||
assert!(content.contains("## High-Level Goal"));
|
||||
assert!(content.contains("## Core Features"));
|
||||
assert!(content.contains("## Domain Definition"));
|
||||
assert!(content.contains("## Glossary"));
|
||||
// Must NOT contain Story Kit-specific content
|
||||
assert!(!content.contains("Agentic AI Code Assistant"));
|
||||
assert!(!content.contains("Poem HTTP server"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_stack_is_blank_template_not_story_kit_content() {
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
let content = fs::read_to_string(dir.path().join(".huskies/specs/tech/STACK.md")).unwrap();
|
||||
assert!(content.contains("<!-- huskies:scaffold-template -->"));
|
||||
assert!(content.contains("## Core Stack"));
|
||||
assert!(content.contains("## Coding Standards"));
|
||||
assert!(content.contains("## Quality Gates"));
|
||||
assert!(content.contains("## Libraries"));
|
||||
// Must NOT contain Story Kit-specific content
|
||||
assert!(!content.contains("Poem HTTP server"));
|
||||
assert!(!content.contains("TypeScript + React"));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn scaffold_story_kit_creates_executable_script_test() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
let script_test = dir.path().join("script/test");
|
||||
assert!(script_test.exists(), "script/test should be created");
|
||||
let perms = fs::metadata(&script_test).unwrap().permissions();
|
||||
assert!(
|
||||
perms.mode() & 0o111 != 0,
|
||||
"script/test should be executable"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_story_kit_does_not_overwrite_existing() {
|
||||
let dir = tempdir().unwrap();
|
||||
let readme = dir.path().join(".huskies/README.md");
|
||||
fs::create_dir_all(readme.parent().unwrap()).unwrap();
|
||||
fs::write(&readme, "custom content").unwrap();
|
||||
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
assert_eq!(fs::read_to_string(&readme).unwrap(), "custom content");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_story_kit_is_idempotent() {
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
let readme_content = fs::read_to_string(dir.path().join(".huskies/README.md")).unwrap();
|
||||
let toml_content = fs::read_to_string(dir.path().join(".huskies/project.toml")).unwrap();
|
||||
|
||||
// Run again — must not change content or add duplicate .gitignore entries
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
fs::read_to_string(dir.path().join(".huskies/README.md")).unwrap(),
|
||||
readme_content
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read_to_string(dir.path().join(".huskies/project.toml")).unwrap(),
|
||||
toml_content
|
||||
);
|
||||
|
||||
let story_kit_gitignore = fs::read_to_string(dir.path().join(".huskies/.gitignore")).unwrap();
|
||||
let count = story_kit_gitignore
|
||||
.lines()
|
||||
.filter(|l| l.trim() == "worktrees/")
|
||||
.count();
|
||||
assert_eq!(
|
||||
count, 1,
|
||||
".huskies/.gitignore should not have duplicate entries"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_story_kit_existing_git_repo_no_commit() {
|
||||
let dir = tempdir().unwrap();
|
||||
|
||||
// Initialize a git repo before scaffold
|
||||
std::process::Command::new("git")
|
||||
.args(["init"])
|
||||
.current_dir(dir.path())
|
||||
.status()
|
||||
.unwrap();
|
||||
std::process::Command::new("git")
|
||||
.args([
|
||||
"-c",
|
||||
"user.email=test@test.com",
|
||||
"-c",
|
||||
"user.name=Test",
|
||||
"commit",
|
||||
"--allow-empty",
|
||||
"-m",
|
||||
"pre-scaffold",
|
||||
])
|
||||
.current_dir(dir.path())
|
||||
.status()
|
||||
.unwrap();
|
||||
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
// Only 1 commit should exist — scaffold must not commit into an existing repo
|
||||
let log_output = std::process::Command::new("git")
|
||||
.args(["log", "--oneline"])
|
||||
.current_dir(dir.path())
|
||||
.output()
|
||||
.unwrap();
|
||||
let log = String::from_utf8_lossy(&log_output.stdout);
|
||||
let commit_count = log.lines().count();
|
||||
assert_eq!(
|
||||
commit_count, 1,
|
||||
"scaffold should not create a commit in an existing git repo"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_creates_story_kit_gitignore_with_relative_entries() {
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
// .huskies/.gitignore must contain relative patterns for files under .huskies/
|
||||
let sk_content = fs::read_to_string(dir.path().join(".huskies/.gitignore")).unwrap();
|
||||
assert!(sk_content.contains("worktrees/"));
|
||||
assert!(sk_content.contains("merge_workspace/"));
|
||||
assert!(sk_content.contains("coverage/"));
|
||||
assert!(sk_content.contains("matrix_history.json"));
|
||||
assert!(sk_content.contains("timers.json"));
|
||||
// Must NOT contain absolute .huskies/ prefixed paths
|
||||
assert!(!sk_content.contains(".huskies/"));
|
||||
|
||||
// Root .gitignore must contain root-level huskies entries
|
||||
let root_content = fs::read_to_string(dir.path().join(".gitignore")).unwrap();
|
||||
assert!(root_content.contains(".huskies_port"));
|
||||
// store.json now lives inside .huskies/ and must NOT appear in root .gitignore
|
||||
assert!(!root_content.contains("store.json"));
|
||||
// Root .gitignore must NOT contain .huskies/ sub-directory patterns
|
||||
assert!(!root_content.contains(".huskies/worktrees/"));
|
||||
assert!(!root_content.contains(".huskies/merge_workspace/"));
|
||||
assert!(!root_content.contains(".huskies/coverage/"));
|
||||
// store.json must be in .huskies/.gitignore instead
|
||||
assert!(sk_content.contains("store.json"));
|
||||
// Database files must be ignored so novice users don't accidentally commit them
|
||||
assert!(sk_content.contains("pipeline.db"));
|
||||
assert!(sk_content.contains("*.db"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_story_kit_gitignore_does_not_duplicate_existing_entries() {
|
||||
let dir = tempdir().unwrap();
|
||||
// Pre-create .huskies dir and .gitignore with some entries already present
|
||||
fs::create_dir_all(dir.path().join(".huskies")).unwrap();
|
||||
fs::write(
|
||||
dir.path().join(".huskies/.gitignore"),
|
||||
"worktrees/\ncoverage/\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
let content = fs::read_to_string(dir.path().join(".huskies/.gitignore")).unwrap();
|
||||
let worktrees_count = content.lines().filter(|l| l.trim() == "worktrees/").count();
|
||||
assert_eq!(worktrees_count, 1, "worktrees/ should not be duplicated");
|
||||
let coverage_count = content.lines().filter(|l| l.trim() == "coverage/").count();
|
||||
assert_eq!(coverage_count, 1, "coverage/ should not be duplicated");
|
||||
// The missing entry must have been added
|
||||
assert!(content.contains("merge_workspace/"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_creates_claude_md_at_project_root() {
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
let claude_md = dir.path().join("CLAUDE.md");
|
||||
assert!(
|
||||
claude_md.exists(),
|
||||
"CLAUDE.md should be created at project root"
|
||||
);
|
||||
|
||||
let content = fs::read_to_string(&claude_md).unwrap();
|
||||
assert!(
|
||||
content.contains("<!-- huskies:scaffold-template -->"),
|
||||
"CLAUDE.md should contain the scaffold sentinel"
|
||||
);
|
||||
assert!(
|
||||
content.contains("Read .huskies/README.md"),
|
||||
"CLAUDE.md should include directive to read .huskies/README.md"
|
||||
);
|
||||
assert!(
|
||||
content.contains("Never chain shell commands"),
|
||||
"CLAUDE.md should include command chaining rule"
|
||||
);
|
||||
assert!(
|
||||
content.contains("wizard_status"),
|
||||
"CLAUDE.md should instruct Claude to call wizard_status on first conversation"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_does_not_overwrite_existing_claude_md() {
|
||||
let dir = tempdir().unwrap();
|
||||
let claude_md = dir.path().join("CLAUDE.md");
|
||||
fs::write(&claude_md, "custom CLAUDE.md content").unwrap();
|
||||
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
fs::read_to_string(&claude_md).unwrap(),
|
||||
"custom CLAUDE.md content",
|
||||
"scaffold should not overwrite an existing CLAUDE.md"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_story_kit_writes_mcp_json_with_port() {
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 4242).unwrap();
|
||||
|
||||
let mcp_path = dir.path().join(".mcp.json");
|
||||
assert!(mcp_path.exists(), ".mcp.json should be created by scaffold");
|
||||
let content = fs::read_to_string(&mcp_path).unwrap();
|
||||
assert!(
|
||||
content.contains("4242"),
|
||||
".mcp.json should reference the given port"
|
||||
);
|
||||
assert!(
|
||||
content.contains("localhost"),
|
||||
".mcp.json should reference localhost"
|
||||
);
|
||||
assert!(
|
||||
content.contains("huskies"),
|
||||
".mcp.json should name the huskies server"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_story_kit_does_not_overwrite_existing_mcp_json() {
|
||||
let dir = tempdir().unwrap();
|
||||
let mcp_path = dir.path().join(".mcp.json");
|
||||
fs::write(&mcp_path, "{\"custom\": true}").unwrap();
|
||||
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
fs::read_to_string(&mcp_path).unwrap(),
|
||||
"{\"custom\": true}",
|
||||
"scaffold should not overwrite an existing .mcp.json"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_gitignore_includes_mcp_json() {
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
let root_gitignore = fs::read_to_string(dir.path().join(".gitignore")).unwrap();
|
||||
assert!(
|
||||
root_gitignore.contains(".mcp.json"),
|
||||
"root .gitignore should include .mcp.json (port is environment-specific)"
|
||||
);
|
||||
}
|
||||
|
||||
#[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"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_story_kit_creates_script_build_and_lint() {
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
assert!(
|
||||
dir.path().join("script/build").exists(),
|
||||
"script/build should be created by scaffold"
|
||||
);
|
||||
assert!(
|
||||
dir.path().join("script/lint").exists(),
|
||||
"script/lint should be created by scaffold"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn scaffold_story_kit_creates_executable_script_build_and_lint() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
for name in &["build", "lint"] {
|
||||
let path = dir.path().join("script").join(name);
|
||||
assert!(path.exists(), "script/{name} should be created");
|
||||
let perms = fs::metadata(&path).unwrap().permissions();
|
||||
assert!(
|
||||
perms.mode() & 0o111 != 0,
|
||||
"script/{name} should be executable"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_script_build_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/build")).unwrap();
|
||||
assert!(
|
||||
content.contains("cargo build --release"),
|
||||
"Rust project scaffold should set cargo build --release in script/build"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_script_lint_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/lint")).unwrap();
|
||||
assert!(
|
||||
content.contains("cargo fmt --all --check"),
|
||||
"Rust project scaffold should include fmt check in script/lint"
|
||||
);
|
||||
assert!(
|
||||
content.contains("cargo clippy -- -D warnings"),
|
||||
"Rust project scaffold should include clippy in script/lint"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_project_toml_contains_detected_components() {
|
||||
let dir = tempdir().unwrap();
|
||||
// Place a Cargo.toml in the project root before scaffolding
|
||||
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(".huskies/project.toml")).unwrap();
|
||||
assert!(
|
||||
content.contains("[[component]]"),
|
||||
"project.toml should contain a component entry"
|
||||
);
|
||||
assert!(
|
||||
content.contains("name = \"server\""),
|
||||
"Rust project should have a 'server' component"
|
||||
);
|
||||
assert!(
|
||||
content.contains("cargo check"),
|
||||
"Rust component should have cargo check setup"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_project_toml_fallback_when_no_stack_detected() {
|
||||
let dir = tempdir().unwrap();
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
let content = fs::read_to_string(dir.path().join(".huskies/project.toml")).unwrap();
|
||||
assert!(
|
||||
content.contains("[[component]]"),
|
||||
"project.toml should always have at least one component"
|
||||
);
|
||||
// Fallback uses generic app component with empty setup — no Rust-specific commands
|
||||
assert!(
|
||||
content.contains("name = \"app\""),
|
||||
"fallback should use generic 'app' component name"
|
||||
);
|
||||
assert!(
|
||||
!content.contains("cargo"),
|
||||
"fallback must not contain Rust-specific commands for non-Rust projects"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_does_not_overwrite_existing_project_toml_with_components() {
|
||||
let dir = tempdir().unwrap();
|
||||
let sk_dir = dir.path().join(".huskies");
|
||||
fs::create_dir_all(&sk_dir).unwrap();
|
||||
let existing = "[[component]]\nname = \"custom\"\npath = \".\"\nsetup = [\"make build\"]\n";
|
||||
fs::write(sk_dir.join("project.toml"), existing).unwrap();
|
||||
|
||||
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||
|
||||
let content = fs::read_to_string(sk_dir.join("project.toml")).unwrap();
|
||||
assert_eq!(
|
||||
content, existing,
|
||||
"scaffold should not overwrite existing project.toml"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user