main-spex 0.25.0

AI-first development framework that turns specifications into deterministic, verifiable code
#[cfg(test)]
mod tests_unit {
    use super::super::*;
    use std::fs;

    use serde_json::Value;
    use tempfile::tempdir;

    use crate::templates::{AGENTS_FILENAME, CLAUDE_FILENAME};

    const SCHEMA: &str = "schemas/spex.schema.json";
    const DOC: &str = "docs/glossary.md";
    const SKILL: &str = ".agents/skills/spex-setup/SKILL.md";
    const SCRIPT: &str = "rules/commit-message-format/commit-spec-id-trace.sh";
    const RULE: &str = "rules/secret-handling.yaml";

    #[test]
    fn scaffold_writes_managed_content() {
        let spex_dir = tempdir().expect("create spex dir");

        scaffold(spex_dir.path(), spex_dir.path()).expect("scaffold");

        for managed in [SCHEMA, DOC, SKILL, SCRIPT, RULE] {
            assert!(
                spex_dir.path().join(managed).is_file(),
                "missing managed file `{managed}`"
            );
        }
    }

    #[test]
    fn scaffold_writes_every_bundled_rule_and_skill() {
        let spex_dir = tempdir().expect("create spex dir");

        scaffold(spex_dir.path(), spex_dir.path()).expect("scaffold");

        let rules = BUNDLED_TEMPLATES_DIR
            .get_dir(RULES_DIR)
            .expect("bundled rules dir");
        for file in rules.files() {
            let dest = spex_dir.path().join(file.path());
            assert_eq!(
                fs::read(&dest).expect("read rule"),
                file.contents(),
                "`{}` differs from the bundled rule",
                dest.display()
            );
        }

        let skills = BUNDLED_TEMPLATES_DIR
            .get_dir(SKILLS_DIRNAME)
            .expect("bundled skills dir");
        for skill in skills.dirs() {
            let name = skill.path().file_name().expect("skill dir name");
            let bundled = skill
                .get_file(skill.path().join("SKILL.md"))
                .expect("bundled SKILL.md");
            let dest = spex_dir
                .path()
                .join(AGENTS_DIRNAME)
                .join(SKILLS_DIRNAME)
                .join(name)
                .join("SKILL.md");
            assert_eq!(
                fs::read(&dest).expect("read skill"),
                bundled.contents(),
                "`{}` differs from the bundled skill",
                dest.display()
            );
        }
    }

    #[test]
    fn scaffold_writes_bundled_docs_only() {
        let spex_dir = tempdir().expect("create spex dir");

        scaffold(spex_dir.path(), spex_dir.path()).expect("scaffold");

        let docs = BUNDLED_TEMPLATES_DIR
            .get_dir(DOCS_DIRNAME)
            .expect("bundled docs dir");
        for file in docs.files() {
            let name = file.path().file_name().expect("doc file name");
            let dest = spex_dir.path().join(DOCS_DIRNAME).join(name);
            assert!(dest.is_file(), "missing bundled doc `{}`", dest.display());
            assert_eq!(
                fs::read(&dest).expect("read doc"),
                file.contents(),
                "`{}` differs from the bundled doc",
                dest.display()
            );
        }
        for name in ["faq.md", "prd.md", "specification.md", "tutorial.md"] {
            assert!(
                !spex_dir.path().join(DOCS_DIRNAME).join(name).exists(),
                "`{name}` is not a bundled doc and must not scaffold"
            );
        }
    }

    #[test]
    fn scaffold_writes_agents_md_and_claude_link() {
        let spex_dir = tempdir().expect("create spex dir");

        scaffold(spex_dir.path(), spex_dir.path()).expect("scaffold");

        let agents = spex_dir.path().join(AGENTS_FILENAME);
        let expected = BUNDLED_TEMPLATES_DIR
            .get_file(AGENTS_FILENAME)
            .expect("bundled AGENTS.md")
            .contents();
        assert_eq!(fs::read(&agents).expect("read AGENTS.md"), expected);

        #[cfg(unix)]
        {
            let claude = spex_dir.path().join(CLAUDE_FILENAME);
            let target = fs::read_link(&claude).expect("read CLAUDE.md symlink");
            assert_eq!(target, Path::new(AGENTS_FILENAME));
        }
    }

    #[test]
    fn bundled_docs_match_the_single_source_of_truth() {
        let root_docs = include_dir::include_dir!("$CARGO_MANIFEST_DIR/docs");
        let bundled = BUNDLED_TEMPLATES_DIR
            .get_dir(DOCS_DIRNAME)
            .expect("bundled docs dir");

        for file in bundled.files() {
            let name = file.path().file_name().expect("doc file name");
            let source = root_docs.get_file(name).unwrap_or_else(|| {
                panic!(
                    "bundled `docs/{}` has no source in the `docs/` folder",
                    name.to_string_lossy()
                )
            });
            assert_eq!(
                file.contents(),
                source.contents(),
                "bundled `docs/{}` differs from `docs/{}`",
                name.to_string_lossy(),
                name.to_string_lossy()
            );
        }
    }

    #[test]
    fn rule_templates_have_no_leftover_template_only_fields() {
        let dir = BUNDLED_TEMPLATES_DIR
            .get_dir(RULES_DIR)
            .expect("bundled rules dir");

        for file in dir.files() {
            let text = file.contents_utf8().expect("utf8 rule");
            assert!(
                !text.contains("first_created_in_spex_version"),
                "{} still carries the removed `first_created_in_spex_version` field",
                file.path().display()
            );
        }
    }

    #[test]
    fn rule_templates_conform_to_schema() {
        let schema_path =
            Path::new(env!("CARGO_MANIFEST_DIR")).join("templates/schemas/rule.schema.json");
        let validator = crate::commands::check_schema::build_validator(&schema_path)
            .expect("build rule validator");

        let dir = BUNDLED_TEMPLATES_DIR
            .get_dir(RULES_DIR)
            .expect("bundled rules dir");

        for file in dir.files() {
            let text = file.contents_utf8().expect("utf8 rule");
            let instance: Value = serde_yaml_ng::from_str(text).expect("parse rule yaml");

            let messages = crate::commands::check_schema::validate_instance(&validator, &instance);
            assert!(
                messages.is_empty(),
                "{} violates rule.schema.json: {messages:?}",
                file.path().display()
            );
        }
    }
}