#![allow(dead_code, unused_imports, unused_variables)]
use anyhow::{bail, Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use crate::interview::InterviewContext;
const RUST_CARGO_TOML: &str = include_str!("../templates/rust/Cargo.toml.template");
const PYTHON_PYPROJECT_TOML: &str = include_str!("../templates/python/pyproject.toml");
const NODEJS_PACKAGE_JSON: &str = include_str!("../templates/nodejs/package.json");
const NODEJS_TSCONFIG_JSON: &str = include_str!("../templates/nodejs/tsconfig.json");
const NODEJS_ESLINT_CONFIG: &str = include_str!("../templates/nodejs/eslint.config.mjs");
const NODEJS_PRETTIERRC: &str = include_str!("../templates/nodejs/.prettierrc");
const NODEJS_VITEST_CONFIG: &str = include_str!("../templates/nodejs/vitest.config.ts");
const WORKFLOW_RUST_QA: &str = include_str!("../templates/workflows/rust-qa.yml");
const WORKFLOW_PYTHON_QA: &str = include_str!("../templates/workflows/python-qa.yml");
const WORKFLOW_NODEJS_QA: &str = include_str!("../templates/workflows/nodejs-qa.yml");
const WORKFLOW_ORCHESTRATOR: &str =
include_str!("../templates/workflows/selfware-qa-orchestrator.yml");
const QA_SCHEMA_YAML: &str = include_str!("../selfware-qa-schema.yaml");
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScaffoldOptions {
pub description: String,
pub framework: Option<String>,
pub with_ci: bool,
pub with_tests: bool,
pub qa_profile: String,
}
impl Default for ScaffoldOptions {
fn default() -> Self {
Self {
description: String::new(),
framework: None,
with_ci: true,
with_tests: true,
qa_profile: "standard".into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TemplateInfo {
pub language: String,
pub description: String,
pub files: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QaSchemaConfig {
pub qa_profile: QaSchemaProfile,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QaSchemaProfile {
pub name: String,
#[serde(default)]
pub description: String,
#[serde(default)]
pub version: String,
#[serde(default)]
pub extends: Option<String>,
#[serde(default)]
pub stages: Vec<QaSchemaStage>,
#[serde(default)]
pub quality_gates: Vec<QaSchemaGate>,
#[serde(default)]
pub scoring: Option<QaSchemaScoring>,
#[serde(default)]
pub coverage: Option<QaSchemaCoverage>,
#[serde(default)]
pub feedback_loops: Option<QaSchemaFeedbackLoops>,
#[serde(default)]
pub language_overrides: Option<HashMap<String, serde_yaml::Value>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QaSchemaStage {
pub name: String,
#[serde(default)]
pub description: String,
#[serde(default = "default_true")]
pub required: bool,
#[serde(default)]
pub fail_fast: bool,
#[serde(default = "default_timeout")]
pub timeout_seconds: u64,
#[serde(default)]
pub coverage_threshold: Option<u64>,
#[serde(default)]
pub severity_threshold: Option<String>,
#[serde(default)]
pub tools: HashMap<String, Vec<QaSchemaTool>>,
}
fn default_true() -> bool {
true
}
fn default_timeout() -> u64 {
60
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QaSchemaTool {
pub command: String,
#[serde(default)]
pub env: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QaSchemaGate {
pub stage: String,
#[serde(default)]
pub fail_on_error: bool,
#[serde(default)]
pub max_warnings: Option<u64>,
#[serde(default)]
pub min_coverage: Option<u64>,
#[serde(default)]
pub severity_threshold: Option<String>,
#[serde(default)]
pub description: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QaSchemaScoring {
#[serde(default)]
pub weights: HashMap<String, f64>,
#[serde(default)]
pub grade_thresholds: HashMap<String, u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QaSchemaCoverage {
#[serde(default)]
pub min_overall: u64,
#[serde(default)]
pub min_per_file: u64,
#[serde(default)]
pub exclude_patterns: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QaSchemaFeedbackLoops {
#[serde(default)]
pub auto_fix: Option<serde_yaml::Value>,
#[serde(default)]
pub retry_with_context: Option<serde_yaml::Value>,
#[serde(default)]
pub escalation: Option<serde_yaml::Value>,
}
pub struct TemplateEngine {
override_dir: Option<PathBuf>,
}
impl Default for TemplateEngine {
fn default() -> Self {
Self::new()
}
}
impl TemplateEngine {
pub fn new() -> Self {
let override_dir = dirs::home_dir()
.map(|h| h.join(".selfware").join("templates"))
.filter(|p| p.is_dir());
Self { override_dir }
}
pub fn with_override_dir(dir: Option<PathBuf>) -> Self {
Self {
override_dir: dir.filter(|p| p.is_dir()),
}
}
pub fn render_template(template: &str, vars: &HashMap<String, String>) -> String {
let mut result = template.to_string();
for (key, value) in vars {
let placeholder = format!("{{{{{}}}}}", key);
result = result.replace(&placeholder, value);
}
result
}
pub fn available_templates() -> Vec<TemplateInfo> {
vec![
TemplateInfo {
language: "rust".into(),
description: "Rust project with Cargo.toml, src/main.rs, src/lib.rs, tests/".into(),
files: vec![
"Cargo.toml".into(),
"src/main.rs".into(),
"src/lib.rs".into(),
"tests/integration_test.rs".into(),
],
},
TemplateInfo {
language: "python".into(),
description: "Python project with pyproject.toml, src/<module>/__init__.py, tests/"
.into(),
files: vec![
"pyproject.toml".into(),
"src/<module>/__init__.py".into(),
"src/<module>/cli.py".into(),
"tests/__init__.py".into(),
"tests/test_main.py".into(),
],
},
TemplateInfo {
language: "nodejs".into(),
description:
"Node.js/TypeScript project with package.json, tsconfig, eslint, vitest".into(),
files: vec![
"package.json".into(),
"tsconfig.json".into(),
"eslint.config.mjs".into(),
".prettierrc".into(),
"vitest.config.ts".into(),
"src/index.ts".into(),
"tests/index.test.ts".into(),
],
},
]
}
fn load_template(&self, relative_path: &str, embedded: &str) -> String {
if let Some(ref dir) = self.override_dir {
let override_path = dir.join(relative_path);
if override_path.is_file() {
if let Ok(content) = std::fs::read_to_string(&override_path) {
return content;
}
}
}
embedded.to_string()
}
pub fn scaffold_project(
&self,
language: &str,
project_name: &str,
project_dir: &Path,
options: &ScaffoldOptions,
) -> Result<Vec<String>> {
let lang = language.to_lowercase();
match lang.as_str() {
"rust" => self.scaffold_rust(project_name, project_dir, options),
"python" => self.scaffold_python(project_name, project_dir, options),
"nodejs" | "node" | "typescript" | "node.js" | "ts" => {
self.scaffold_nodejs(project_name, project_dir, options)
}
other => bail!(
"Unsupported language '{}'. Supported: rust, python, nodejs",
other
),
}
}
fn build_vars(&self, project_name: &str, options: &ScaffoldOptions) -> HashMap<String, String> {
let module_name = project_name.replace('-', "_");
let mut vars = HashMap::new();
vars.insert("project_name".into(), project_name.into());
vars.insert("project_description".into(), options.description.clone());
vars.insert("module_name".into(), module_name);
vars.insert("repository_url".into(), String::new());
vars.insert("project_url".into(), String::new());
vars.insert("docs_url".into(), String::new());
vars.insert("keywords".into(), String::new());
vars.insert("categories".into(), String::new());
vars
}
fn write_file(
project_dir: &Path,
relative: &str,
content: &str,
created: &mut Vec<String>,
) -> Result<()> {
let full = project_dir.join(relative);
if let Some(parent) = full.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("creating directory {}", parent.display()))?;
}
std::fs::write(&full, content).with_context(|| format!("writing {}", full.display()))?;
created.push(relative.to_string());
Ok(())
}
fn scaffold_rust(
&self,
project_name: &str,
project_dir: &Path,
options: &ScaffoldOptions,
) -> Result<Vec<String>> {
let vars = self.build_vars(project_name, options);
let mut created = Vec::new();
let cargo_tmpl = self.load_template("rust/Cargo.toml", RUST_CARGO_TOML);
let cargo_content = Self::render_template(&cargo_tmpl, &vars);
Self::write_file(project_dir, "Cargo.toml", &cargo_content, &mut created)?;
let main_rs = format!(
r#"use anyhow::Result;
fn main() -> Result<()> {{
println!("Hello from {}!");
Ok(())
}}
"#,
project_name
);
Self::write_file(project_dir, "src/main.rs", &main_rs, &mut created)?;
let lib_rs = format!(
r#"//! {} - {}
pub fn greet() -> &'static str {{
"Hello from {}!"
}}
"#,
project_name, options.description, project_name
);
Self::write_file(project_dir, "src/lib.rs", &lib_rs, &mut created)?;
if options.with_tests {
let test_rs = format!(
r#"use {}::greet;
#[test]
fn test_greet() {{
assert!(greet().contains("{}"));
}}
"#,
project_name.replace('-', "_"),
project_name,
);
Self::write_file(
project_dir,
"tests/integration_test.rs",
&test_rs,
&mut created,
)?;
}
if options.with_ci {
let wf = self.load_template("workflows/rust-qa.yml", WORKFLOW_RUST_QA);
Self::write_file(
project_dir,
".github/workflows/rust-qa.yml",
&wf,
&mut created,
)?;
}
Ok(created)
}
fn scaffold_python(
&self,
project_name: &str,
project_dir: &Path,
options: &ScaffoldOptions,
) -> Result<Vec<String>> {
let vars = self.build_vars(project_name, options);
let module_name = project_name.replace('-', "_");
let mut created = Vec::new();
let pyproject_tmpl = self.load_template("python/pyproject.toml", PYTHON_PYPROJECT_TOML);
let pyproject_content = Self::render_template(&pyproject_tmpl, &vars);
Self::write_file(
project_dir,
"pyproject.toml",
&pyproject_content,
&mut created,
)?;
let init_py = format!(
r#""""{} - {}""""
__version__ = "0.1.0"
def main() -> None:
"""Entry point."""
print("Hello from {}!")
"#,
module_name, options.description, project_name
);
Self::write_file(
project_dir,
&format!("src/{}/__init__.py", module_name),
&init_py,
&mut created,
)?;
let cli_py = format!(
r#"""Command-line interface for {}.""
import argparse
from . import main
def cli() -> None:
"""Parse arguments and run."""
parser = argparse.ArgumentParser(description="{}")
_ = parser.parse_args()
main()
if __name__ == "__main__":
cli()
"#,
project_name, options.description
);
Self::write_file(
project_dir,
&format!("src/{}/cli.py", module_name),
&cli_py,
&mut created,
)?;
if options.with_tests {
Self::write_file(project_dir, "tests/__init__.py", "", &mut created)?;
let test_py = format!(
r#"""Tests for {}.""
from {} import main
def test_main(capsys):
"""Test that main runs without error."""
main()
captured = capsys.readouterr()
assert "{}" in captured.out
"#,
project_name, module_name, project_name
);
Self::write_file(project_dir, "tests/test_main.py", &test_py, &mut created)?;
}
if options.with_ci {
let wf = self.load_template("workflows/python-qa.yml", WORKFLOW_PYTHON_QA);
Self::write_file(
project_dir,
".github/workflows/python-qa.yml",
&wf,
&mut created,
)?;
}
Ok(created)
}
fn scaffold_nodejs(
&self,
project_name: &str,
project_dir: &Path,
options: &ScaffoldOptions,
) -> Result<Vec<String>> {
let vars = self.build_vars(project_name, options);
let mut created = Vec::new();
let pkg_tmpl = self.load_template("nodejs/package.json", NODEJS_PACKAGE_JSON);
let pkg_content = Self::render_template(&pkg_tmpl, &vars);
Self::write_file(project_dir, "package.json", &pkg_content, &mut created)?;
let tsconfig = self.load_template("nodejs/tsconfig.json", NODEJS_TSCONFIG_JSON);
Self::write_file(project_dir, "tsconfig.json", &tsconfig, &mut created)?;
let eslint = self.load_template("nodejs/eslint.config.mjs", NODEJS_ESLINT_CONFIG);
Self::write_file(project_dir, "eslint.config.mjs", &eslint, &mut created)?;
let prettier = self.load_template("nodejs/.prettierrc", NODEJS_PRETTIERRC);
Self::write_file(project_dir, ".prettierrc", &prettier, &mut created)?;
let vitest = self.load_template("nodejs/vitest.config.ts", NODEJS_VITEST_CONFIG);
Self::write_file(project_dir, "vitest.config.ts", &vitest, &mut created)?;
let index_ts = format!(
r#"/**
* {} - {}
*/
export function greet(): string {{
return "Hello from {}!";
}}
console.log(greet());
"#,
project_name, options.description, project_name
);
Self::write_file(project_dir, "src/index.ts", &index_ts, &mut created)?;
if options.with_tests {
let test_ts = format!(
r#"import {{ describe, it, expect }} from "vitest";
import {{ greet }} from "../src/index";
describe("{}", () => {{
it("should greet correctly", () => {{
const result = greet();
expect(result).toContain("{}");
}});
}});
"#,
project_name, project_name,
);
Self::write_file(project_dir, "tests/index.test.ts", &test_ts, &mut created)?;
}
if options.with_ci {
let wf = self.load_template("workflows/nodejs-qa.yml", WORKFLOW_NODEJS_QA);
Self::write_file(
project_dir,
".github/workflows/nodejs-qa.yml",
&wf,
&mut created,
)?;
}
Ok(created)
}
}
pub fn load_qa_schema(path: Option<&Path>) -> Result<QaSchemaConfig> {
load_qa_schema_profile(path, "standard")
}
pub fn load_qa_schema_profile(path: Option<&Path>, profile_name: &str) -> Result<QaSchemaConfig> {
let content = match path {
Some(p) => std::fs::read_to_string(p)
.with_context(|| format!("reading QA schema from {}", p.display()))?,
None => QA_SCHEMA_YAML.to_string(),
};
for document in serde_yaml::Deserializer::from_str(&content) {
if let Ok(config) = QaSchemaConfig::deserialize(document) {
if config.qa_profile.name == profile_name {
return Ok(config);
}
}
}
bail!(
"QA profile '{}' not found in schema. Available: standard, strict, minimal",
profile_name
)
}
pub fn qa_schema_to_weights(schema: &QaSchemaConfig) -> crate::testing::qa_profiles::QaWeights {
let defaults = crate::testing::qa_profiles::QaWeights::standard();
let scoring = match &schema.qa_profile.scoring {
Some(s) => &s.weights,
None => return defaults,
};
let get =
|key: &str, fallback: f64| -> f64 { scoring.get(key).copied().unwrap_or(fallback) * 100.0 };
crate::testing::qa_profiles::QaWeights {
syntax: get("syntax", 0.10),
format: get("format", 0.05),
lint: get("lint", 0.15),
type_check: get("typecheck", 0.10),
test: get("test", 0.30),
security: get("security", 0.10),
}
}
pub fn scaffold_from_context(ctx: &InterviewContext, project_dir: &Path) -> Result<Vec<String>> {
let language = ctx.language.as_deref().unwrap_or("rust").to_lowercase();
let lang_key = if language.contains("typescript") || language.contains("node") {
"nodejs"
} else if language.contains("python") {
"python"
} else if language.contains("rust") {
"rust"
} else {
&language
};
let project_name = project_dir
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("my-project")
.to_string();
let qa_profile = match ctx.testing_preference {
Some(crate::interview::TestingPreference::Tdd) => "strict",
Some(crate::interview::TestingPreference::Minimal) => "minimal",
Some(crate::interview::TestingPreference::None) => "minimal",
_ => "standard",
};
let with_tests = !matches!(
ctx.testing_preference,
Some(crate::interview::TestingPreference::None)
);
let description = if ctx.task.is_empty() {
ctx.extra_notes.first().cloned().unwrap_or_default()
} else {
ctx.task.clone()
};
let options = ScaffoldOptions {
description,
framework: ctx.framework.clone(),
with_ci: true,
with_tests,
qa_profile: qa_profile.into(),
};
let engine = TemplateEngine::new();
engine.scaffold_project(lang_key, &project_name, project_dir, &options)
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
use tempfile::TempDir;
#[test]
fn test_render_template_basic() {
let mut vars = HashMap::new();
vars.insert("name".into(), "hello-world".into());
vars.insert("desc".into(), "A test project".into());
let result = TemplateEngine::render_template("name={{name}}, desc={{desc}}", &vars);
assert_eq!(result, "name=hello-world, desc=A test project");
}
#[test]
fn test_render_template_missing_placeholder_kept() {
let vars = HashMap::new();
let result = TemplateEngine::render_template("{{unknown}}", &vars);
assert_eq!(result, "{{unknown}}");
}
#[test]
fn test_render_template_multiple_occurrences() {
let mut vars = HashMap::new();
vars.insert("x".into(), "42".into());
let result = TemplateEngine::render_template("a={{x}} b={{x}}", &vars);
assert_eq!(result, "a=42 b=42");
}
#[test]
fn test_render_template_empty_value() {
let mut vars = HashMap::new();
vars.insert("project_name".into(), "".into());
let result = TemplateEngine::render_template("name={{project_name}}", &vars);
assert_eq!(result, "name=");
}
#[test]
fn test_render_template_no_placeholders() {
let vars = HashMap::new();
let result = TemplateEngine::render_template("plain text", &vars);
assert_eq!(result, "plain text");
}
#[test]
fn test_scaffold_rust_project() {
let dir = TempDir::new().unwrap();
let engine = TemplateEngine::with_override_dir(None);
let opts = ScaffoldOptions {
description: "Test Rust project".into(),
framework: None,
with_ci: true,
with_tests: true,
qa_profile: "standard".into(),
};
let files = engine
.scaffold_project("rust", "my-app", dir.path(), &opts)
.unwrap();
let cargo_path = dir.path().join("Cargo.toml");
assert!(cargo_path.exists(), "Cargo.toml should exist");
let cargo_content = std::fs::read_to_string(&cargo_path).unwrap();
assert!(
cargo_content.contains("name = \"my-app\""),
"Cargo.toml should contain project name"
);
assert!(
cargo_content.contains("Test Rust project"),
"Cargo.toml should contain description"
);
assert!(dir.path().join("src/main.rs").exists());
let main_content = std::fs::read_to_string(dir.path().join("src/main.rs")).unwrap();
assert!(main_content.contains("my-app"));
assert!(dir.path().join("src/lib.rs").exists());
assert!(dir.path().join("tests/integration_test.rs").exists());
assert!(dir.path().join(".github/workflows/rust-qa.yml").exists());
assert!(files.contains(&"Cargo.toml".to_string()));
assert!(files.contains(&"src/main.rs".to_string()));
assert!(files.contains(&"src/lib.rs".to_string()));
assert!(files.contains(&"tests/integration_test.rs".to_string()));
assert!(files.contains(&".github/workflows/rust-qa.yml".to_string()));
}
#[test]
fn test_scaffold_python_project() {
let dir = TempDir::new().unwrap();
let engine = TemplateEngine::with_override_dir(None);
let opts = ScaffoldOptions {
description: "Test Python project".into(),
framework: None,
with_ci: true,
with_tests: true,
qa_profile: "standard".into(),
};
let files = engine
.scaffold_project("python", "my-api", dir.path(), &opts)
.unwrap();
let pyproject_path = dir.path().join("pyproject.toml");
assert!(pyproject_path.exists(), "pyproject.toml should exist");
let pyproject_content = std::fs::read_to_string(&pyproject_path).unwrap();
assert!(
pyproject_content.contains("name = \"my-api\""),
"pyproject.toml should contain project name"
);
assert!(dir.path().join("src/my_api/__init__.py").exists());
assert!(dir.path().join("src/my_api/cli.py").exists());
assert!(dir.path().join("tests/__init__.py").exists());
assert!(dir.path().join("tests/test_main.py").exists());
assert!(dir.path().join(".github/workflows/python-qa.yml").exists());
assert!(files.contains(&"pyproject.toml".to_string()));
assert!(files.contains(&"src/my_api/__init__.py".to_string()));
}
#[test]
fn test_scaffold_nodejs_project() {
let dir = TempDir::new().unwrap();
let engine = TemplateEngine::with_override_dir(None);
let opts = ScaffoldOptions {
description: "Test Node project".into(),
framework: None,
with_ci: true,
with_tests: true,
qa_profile: "standard".into(),
};
let files = engine
.scaffold_project("nodejs", "my-service", dir.path(), &opts)
.unwrap();
assert!(dir.path().join("package.json").exists());
assert!(dir.path().join("tsconfig.json").exists());
assert!(dir.path().join("eslint.config.mjs").exists());
assert!(dir.path().join(".prettierrc").exists());
assert!(dir.path().join("vitest.config.ts").exists());
assert!(dir.path().join("src/index.ts").exists());
assert!(dir.path().join("tests/index.test.ts").exists());
let pkg_content = std::fs::read_to_string(dir.path().join("package.json")).unwrap();
assert!(pkg_content.contains("\"name\": \"my-service\""));
assert!(dir.path().join(".github/workflows/nodejs-qa.yml").exists());
assert!(files.contains(&"package.json".to_string()));
assert!(files.contains(&"tsconfig.json".to_string()));
assert!(files.contains(&"eslint.config.mjs".to_string()));
assert!(files.contains(&".prettierrc".to_string()));
assert!(files.contains(&"vitest.config.ts".to_string()));
}
#[test]
fn test_scaffold_nodejs_aliases() {
let dir = TempDir::new().unwrap();
let engine = TemplateEngine::with_override_dir(None);
let opts = ScaffoldOptions::default();
for alias in &["nodejs", "node", "typescript", "node.js", "ts"] {
let sub = dir.path().join(alias);
std::fs::create_dir_all(&sub).unwrap();
let result = engine.scaffold_project(alias, "test", &sub, &opts);
assert!(
result.is_ok(),
"alias '{}' should succeed: {:?}",
alias,
result.err()
);
}
}
#[test]
fn test_load_qa_schema_embedded() {
let config = load_qa_schema(None).unwrap();
assert_eq!(config.qa_profile.name, "standard");
assert!(!config.qa_profile.stages.is_empty());
assert!(!config.qa_profile.quality_gates.is_empty());
}
#[test]
fn test_load_qa_schema_from_disk() {
let dir = TempDir::new().unwrap();
let schema_path = dir.path().join("qa-schema.yaml");
std::fs::write(&schema_path, QA_SCHEMA_YAML).unwrap();
let config = load_qa_schema_profile(Some(&schema_path), "standard").unwrap();
assert_eq!(config.qa_profile.name, "standard");
}
#[test]
fn test_load_qa_schema_profile_standard() {
let config = load_qa_schema_profile(None, "standard").unwrap();
assert_eq!(config.qa_profile.name, "standard");
}
#[test]
fn test_load_qa_schema_profile_strict() {
let config = load_qa_schema_profile(None, "strict").unwrap();
assert_eq!(config.qa_profile.name, "strict");
}
#[test]
fn test_load_qa_schema_profile_minimal() {
let config = load_qa_schema_profile(None, "minimal").unwrap();
assert_eq!(config.qa_profile.name, "minimal");
}
#[test]
fn test_load_qa_schema_profile_unknown() {
let result = load_qa_schema_profile(None, "nonexistent");
assert!(result.is_err());
}
#[test]
fn test_qa_schema_to_weights() {
let config = load_qa_schema(None).unwrap();
let weights = qa_schema_to_weights(&config);
assert!((weights.syntax - 10.0).abs() < 0.01);
assert!((weights.test - 30.0).abs() < 0.01);
assert!(weights.total() > 0.0);
}
#[test]
fn test_embedded_rust_cargo_toml_is_valid_toml() {
let mut vars = HashMap::new();
vars.insert("project_name".into(), "test_project".into());
vars.insert("project_description".into(), "test".into());
vars.insert("repository_url".into(), "".into());
vars.insert("project_url".into(), "".into());
vars.insert("keywords".into(), "test".into());
vars.insert("categories".into(), "test".into());
let rendered = TemplateEngine::render_template(RUST_CARGO_TOML, &vars);
let parsed: Result<toml::Value, _> = toml::from_str(&rendered);
assert!(
parsed.is_ok(),
"Rendered Cargo.toml should be valid TOML: {:?}",
parsed.err()
);
}
#[test]
fn test_embedded_nodejs_package_json_is_valid_json() {
let mut vars = HashMap::new();
vars.insert("project_name".into(), "test-project".into());
vars.insert("project_description".into(), "test".into());
vars.insert("repository_url".into(), "".into());
vars.insert("project_url".into(), "".into());
vars.insert("keywords".into(), "test".into());
let rendered = TemplateEngine::render_template(NODEJS_PACKAGE_JSON, &vars);
let parsed: Result<serde_json::Value, _> = serde_json::from_str(&rendered);
assert!(
parsed.is_ok(),
"Rendered package.json should be valid JSON: {:?}",
parsed.err()
);
}
#[test]
fn test_embedded_prettierrc_is_valid_json() {
let parsed: Result<serde_json::Value, _> = serde_json::from_str(NODEJS_PRETTIERRC);
assert!(
parsed.is_ok(),
".prettierrc should be valid JSON: {:?}",
parsed.err()
);
}
#[test]
fn test_embedded_tsconfig_is_parseable() {
assert!(NODEJS_TSCONFIG_JSON.contains("compilerOptions"));
assert!(NODEJS_TSCONFIG_JSON.contains("\"strict\": true"));
assert!(NODEJS_TSCONFIG_JSON.contains("\"outDir\""));
}
#[test]
fn test_embedded_qa_schema_is_valid_yaml() {
let mut count = 0;
for document in serde_yaml::Deserializer::from_str(QA_SCHEMA_YAML) {
let val = serde_yaml::Value::deserialize(document);
assert!(
val.is_ok(),
"YAML document {} should parse: {:?}",
count,
val.err()
);
count += 1;
}
assert!(
count >= 3,
"Should have at least 3 YAML documents (standard, strict, minimal)"
);
}
#[test]
fn test_ci_workflow_generation_rust() {
let dir = TempDir::new().unwrap();
let engine = TemplateEngine::with_override_dir(None);
let opts = ScaffoldOptions {
with_ci: true,
..Default::default()
};
let files = engine
.scaffold_project("rust", "ci-test", dir.path(), &opts)
.unwrap();
assert!(files.contains(&".github/workflows/rust-qa.yml".to_string()));
let wf_content =
std::fs::read_to_string(dir.path().join(".github/workflows/rust-qa.yml")).unwrap();
assert!(wf_content.contains("cargo check"));
assert!(wf_content.contains("cargo clippy"));
}
#[test]
fn test_ci_workflow_generation_python() {
let dir = TempDir::new().unwrap();
let engine = TemplateEngine::with_override_dir(None);
let opts = ScaffoldOptions {
with_ci: true,
..Default::default()
};
let files = engine
.scaffold_project("python", "ci-test", dir.path(), &opts)
.unwrap();
assert!(files.contains(&".github/workflows/python-qa.yml".to_string()));
}
#[test]
fn test_ci_workflow_generation_nodejs() {
let dir = TempDir::new().unwrap();
let engine = TemplateEngine::with_override_dir(None);
let opts = ScaffoldOptions {
with_ci: true,
..Default::default()
};
let files = engine
.scaffold_project("nodejs", "ci-test", dir.path(), &opts)
.unwrap();
assert!(files.contains(&".github/workflows/nodejs-qa.yml".to_string()));
}
#[test]
fn test_no_ci_when_disabled() {
let dir = TempDir::new().unwrap();
let engine = TemplateEngine::with_override_dir(None);
let opts = ScaffoldOptions {
with_ci: false,
..Default::default()
};
let files = engine
.scaffold_project("rust", "no-ci", dir.path(), &opts)
.unwrap();
assert!(!files.iter().any(|f| f.contains(".github")));
}
#[test]
fn test_no_tests_when_disabled() {
let dir = TempDir::new().unwrap();
let engine = TemplateEngine::with_override_dir(None);
let opts = ScaffoldOptions {
with_tests: false,
..Default::default()
};
let files = engine
.scaffold_project("rust", "no-tests", dir.path(), &opts)
.unwrap();
assert!(!files.iter().any(|f| f.contains("tests/")));
}
#[test]
fn test_scaffold_from_context_rust() {
use crate::interview::{InterviewContext, ProjectType, TestingPreference};
let dir = TempDir::new().unwrap();
let project_dir = dir.path().join("my-rust-app");
std::fs::create_dir_all(&project_dir).unwrap();
let ctx = InterviewContext {
language: Some("Rust".into()),
framework: Some("axum".into()),
project_type: Some(ProjectType::WebApi),
testing_preference: Some(TestingPreference::TestsAfter),
output_dir: None,
scope: None,
extra_notes: vec![],
task: "Build a REST API".into(),
};
let files = scaffold_from_context(&ctx, &project_dir).unwrap();
assert!(files.contains(&"Cargo.toml".to_string()));
assert!(files.contains(&"src/main.rs".to_string()));
}
#[test]
fn test_scaffold_from_context_python() {
use crate::interview::{InterviewContext, TestingPreference};
let dir = TempDir::new().unwrap();
let project_dir = dir.path().join("my-python-app");
std::fs::create_dir_all(&project_dir).unwrap();
let ctx = InterviewContext {
language: Some("Python".into()),
framework: None,
project_type: None,
testing_preference: Some(TestingPreference::Tdd),
output_dir: None,
scope: None,
extra_notes: vec![],
task: "A Python service".into(),
};
let files = scaffold_from_context(&ctx, &project_dir).unwrap();
assert!(files.contains(&"pyproject.toml".to_string()));
}
#[test]
fn test_scaffold_from_context_no_tests() {
use crate::interview::{InterviewContext, TestingPreference};
let dir = TempDir::new().unwrap();
let project_dir = dir.path().join("no-tests-app");
std::fs::create_dir_all(&project_dir).unwrap();
let ctx = InterviewContext {
language: Some("Rust".into()),
framework: None,
project_type: None,
testing_preference: Some(TestingPreference::None),
output_dir: None,
scope: None,
extra_notes: vec![],
task: "Quick script".into(),
};
let files = scaffold_from_context(&ctx, &project_dir).unwrap();
assert!(!files.iter().any(|f| f.contains("tests/")));
}
#[test]
fn test_available_templates() {
let templates = TemplateEngine::available_templates();
assert_eq!(templates.len(), 3);
assert!(templates.iter().any(|t| t.language == "rust"));
assert!(templates.iter().any(|t| t.language == "python"));
assert!(templates.iter().any(|t| t.language == "nodejs"));
}
#[test]
fn test_scaffold_unsupported_language() {
let dir = TempDir::new().unwrap();
let engine = TemplateEngine::with_override_dir(None);
let opts = ScaffoldOptions::default();
let result = engine.scaffold_project("haskell", "test", dir.path(), &opts);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("Unsupported language"));
}
#[test]
fn test_runtime_override() {
let dir = TempDir::new().unwrap();
let override_dir = dir.path().join("overrides");
let rust_dir = override_dir.join("rust");
std::fs::create_dir_all(&rust_dir).unwrap();
std::fs::write(
rust_dir.join("Cargo.toml"),
"[package]\nname = \"{{project_name}}\"\nversion = \"99.0.0\"\nedition = \"2021\"\n",
)
.unwrap();
let engine = TemplateEngine::with_override_dir(Some(override_dir));
let opts = ScaffoldOptions::default();
let project_dir = dir.path().join("project");
std::fs::create_dir_all(&project_dir).unwrap();
let files = engine
.scaffold_project("rust", "overridden", &project_dir, &opts)
.unwrap();
let cargo_content = std::fs::read_to_string(project_dir.join("Cargo.toml")).unwrap();
assert!(
cargo_content.contains("99.0.0"),
"Should use the overridden template"
);
}
}