main-spex 0.20.0

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

    use tempfile::tempdir;

    use crate::commands::init::scaffold::scaffold;
    use crate::support::managed_section::PROJECT_SECTION_MARKER;

    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";
    const MANAGED_SPEC: &str = "rules/commit-message-format.yaml";
    const MANAGED_SPEC_ID: &str = "commit-message-format";

    #[test]
    fn sync_overwrites_modified_managed_file() {
        let spex_dir = tempdir().expect("create spex dir");
        scaffold(spex_dir.path(), spex_dir.path()).expect("scaffold");

        let schema = spex_dir.path().join(SCHEMA);
        fs::write(&schema, "tampered").expect("modify schema");

        sync_templates(spex_dir.path(), spex_dir.path(), &[]).expect("sync");

        let expected = BUNDLED_TEMPLATES_DIR
            .get_file(SCHEMA)
            .expect("bundled schema")
            .contents();
        assert_eq!(fs::read(&schema).expect("read schema"), expected);
    }

    #[test]
    fn sync_overwrites_modified_managed_doc() {
        let spex_dir = tempdir().expect("create spex dir");
        scaffold(spex_dir.path(), spex_dir.path()).expect("scaffold");

        let doc = spex_dir.path().join(DOC);
        fs::write(&doc, "tampered").expect("modify doc");

        sync_templates(spex_dir.path(), spex_dir.path(), &[]).expect("sync");

        let expected = BUNDLED_TEMPLATES_DIR
            .get_file(DOC)
            .expect("bundled doc")
            .contents();
        assert_eq!(fs::read(&doc).expect("read doc"), expected);
    }

    #[test]
    fn sync_overwrites_rule_marked_managed() {
        let spex_dir = tempdir().expect("create spex dir");
        scaffold(spex_dir.path(), spex_dir.path()).expect("scaffold");

        let spec = spex_dir.path().join(MANAGED_SPEC);
        fs::write(
            &spec,
            "id: commit-message-format\nsync_with_upstream: true\ntampered: true\n",
        )
        .expect("tamper spec, keeping sync_with_upstream");
        let script = spex_dir.path().join(SCRIPT);
        fs::write(&script, "tampered").expect("tamper script");

        sync_templates(spex_dir.path(), spex_dir.path(), &[]).expect("sync");

        let expected_spec = BUNDLED_TEMPLATES_DIR
            .get_file(MANAGED_SPEC)
            .expect("bundled spec")
            .contents();
        assert_eq!(fs::read(&spec).expect("read spec"), expected_spec);
        let expected_script = BUNDLED_TEMPLATES_DIR
            .get_file(SCRIPT)
            .expect("bundled script")
            .contents();
        assert_eq!(fs::read(&script).expect("read script"), expected_script);
    }

    #[test]
    fn sync_preserves_unmanaged_rule() {
        let spex_dir = tempdir().expect("create spex dir");
        scaffold(spex_dir.path(), spex_dir.path()).expect("scaffold");

        let spec = spex_dir.path().join(MANAGED_SPEC);
        fs::write(&spec, "id: commit-message-format\ncustomized: true\n").expect("customize spec");
        let script = spex_dir.path().join(SCRIPT);
        fs::write(&script, "customized").expect("customize script");

        sync_templates(spex_dir.path(), spex_dir.path(), &[]).expect("sync");

        assert_eq!(
            fs::read_to_string(&spec).unwrap(),
            "id: commit-message-format\ncustomized: true\n"
        );
        assert_eq!(fs::read_to_string(&script).unwrap(), "customized");
    }

    #[test]
    fn sync_leaves_a_deliberately_deleted_rule_deleted() {
        let spex_dir = tempdir().expect("create spex dir");
        scaffold(spex_dir.path(), spex_dir.path()).expect("scaffold");

        let spec = spex_dir.path().join(MANAGED_SPEC);
        fs::remove_file(&spec).expect("delete rule");

        sync_templates(
            spex_dir.path(),
            spex_dir.path(),
            &[MANAGED_SPEC_ID.to_string()],
        )
        .expect("sync");

        assert!(
            !spec.exists(),
            "deliberately deleted rule should stay deleted"
        );
    }

    #[test]
    fn sync_deploys_a_missing_rule_not_marked_removed() {
        let spex_dir = tempdir().expect("create spex dir");
        scaffold(spex_dir.path(), spex_dir.path()).expect("scaffold");

        let spec = spex_dir.path().join(MANAGED_SPEC);
        fs::remove_file(&spec).expect("delete rule");

        sync_templates(spex_dir.path(), spex_dir.path(), &[]).expect("sync");

        assert!(
            spec.exists(),
            "rule missing but not marked removed should be deployed"
        );
    }

    #[test]
    fn sync_does_not_recreate_a_rule_moved_into_a_nested_folder() {
        let spex_dir = tempdir().expect("create spex dir");
        scaffold(spex_dir.path(), spex_dir.path()).expect("scaffold");

        let flat_rule = spex_dir.path().join(RULE);
        let nested_rule = spex_dir
            .path()
            .join("rules")
            .join("security")
            .join("secret-handling.yaml");
        fs::create_dir_all(nested_rule.parent().unwrap()).expect("create nested folder");
        fs::rename(&flat_rule, &nested_rule).expect("move rule into nested folder");

        sync_templates(spex_dir.path(), spex_dir.path(), &[]).expect("sync");

        assert!(
            !flat_rule.exists(),
            "rule already present in a nested folder should not be recreated flat"
        );
        assert!(nested_rule.exists(), "nested rule should still be present");
    }

    #[test]
    fn sync_writes_the_scripts_of_a_nested_rule_beside_it() {
        let spex_dir = tempdir().expect("create spex dir");
        scaffold(spex_dir.path(), spex_dir.path()).expect("scaffold");

        let rules = spex_dir.path().join("rules");
        let nested_rule = rules.join("policy").join("commit-message-format.yaml");
        fs::create_dir_all(nested_rule.parent().unwrap()).expect("create nested folder");
        fs::rename(spex_dir.path().join(MANAGED_SPEC), &nested_rule)
            .expect("move rule into nested folder");
        fs::remove_dir_all(rules.join(MANAGED_SPEC_ID)).expect("remove flat script folder");

        sync_templates(spex_dir.path(), spex_dir.path(), &[]).expect("sync");

        assert!(
            nested_rule
                .with_extension("")
                .join("commit-spec-id-trace.sh")
                .is_file(),
            "script should be written beside the nested rule"
        );
        assert!(
            !rules.join(MANAGED_SPEC_ID).exists(),
            "script folder should not be recreated beside the flat rule path"
        );
    }

    #[test]
    fn sync_preserves_additional_files() {
        let spex_dir = tempdir().expect("create spex dir");
        scaffold(spex_dir.path(), spex_dir.path()).expect("scaffold");

        let extra_skill = spex_dir
            .path()
            .join(AGENTS_DIRNAME)
            .join(SKILLS_DIRNAME)
            .join("custom-skill")
            .join(SKILL_FILE);
        fs::create_dir_all(extra_skill.parent().unwrap()).expect("create custom skill dir");
        fs::write(&extra_skill, "custom").expect("write custom skill");

        let extra_script = spex_dir
            .path()
            .join("rules/commit-message-format/custom.sh");
        fs::write(&extra_script, "echo custom").expect("write custom script");

        sync_templates(spex_dir.path(), spex_dir.path(), &[]).expect("sync");

        assert_eq!(fs::read_to_string(&extra_skill).unwrap(), "custom");
        assert_eq!(fs::read_to_string(&extra_script).unwrap(), "echo custom");
    }

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

        sync_templates(spex_dir.path(), spex_dir.path(), &[]).expect("sync");

        assert!(
            !spex_dir.path().join(AGENTS_FILENAME).exists(),
            "sync (update) must not create AGENTS.md"
        );
    }

    #[test]
    fn sync_syncs_managed_section_of_agents_md_preserving_project_data() {
        let spex_dir = tempdir().expect("create spex dir");
        let agents = spex_dir.path().join(AGENTS_FILENAME);
        fs::write(
            &agents,
            "stale managed content\n\n---\n\n# Project\n\nmy custom notes\n",
        )
        .expect("seed AGENTS.md");

        sync_templates(spex_dir.path(), spex_dir.path(), &[]).expect("sync");

        let updated = fs::read_to_string(&agents).expect("read AGENTS.md");
        let template = BUNDLED_TEMPLATES_DIR
            .get_file(AGENTS_FILENAME)
            .expect("bundled AGENTS.md")
            .contents_utf8()
            .expect("utf8 AGENTS.md");
        let (managed, _) = template
            .split_once("\n---\n\n# Project")
            .expect("bundled AGENTS.md has the project marker");

        assert!(updated.starts_with(managed), "managed section not synced");
        assert!(
            updated.ends_with("# Project\n\nmy custom notes\n"),
            "project-specific section not preserved"
        );
    }

    #[test]
    fn sync_overwrites_agents_md_without_marker() {
        let spex_dir = tempdir().expect("create spex dir");
        let agents = spex_dir.path().join(AGENTS_FILENAME);
        fs::write(&agents, "a pre-split AGENTS.md with no marker\n").expect("seed AGENTS.md");

        sync_templates(spex_dir.path(), spex_dir.path(), &[]).expect("sync");

        let expected = BUNDLED_TEMPLATES_DIR
            .get_file(AGENTS_FILENAME)
            .expect("bundled AGENTS.md")
            .contents();
        assert_eq!(
            fs::read(&agents).expect("read AGENTS.md"),
            expected,
            "AGENTS.md without the marker must be fully overwritten"
        );
    }

    #[test]
    fn sync_restores_bundled_skill_files_and_keeps_extra_ones() {
        let spex_dir = tempdir().expect("create spex dir");
        scaffold(spex_dir.path(), spex_dir.path()).expect("scaffold");

        let skill_dir = spex_dir
            .path()
            .join(AGENTS_DIRNAME)
            .join(SKILLS_DIRNAME)
            .join("spex-setup");
        let bundled_reference = skill_dir.join("references/GENERIC-PACKAGE-SKILL-TEMPLATE.md");
        let extra = skill_dir.join("notes.md");
        fs::write(&bundled_reference, "tampered").expect("tamper bundled reference");
        fs::write(&extra, "my notes").expect("write extra file");

        sync_templates(spex_dir.path(), spex_dir.path(), &[]).expect("sync");

        let expected = BUNDLED_TEMPLATES_DIR
            .get_file("skills/spex-setup/references/GENERIC-PACKAGE-SKILL-TEMPLATE.md")
            .expect("bundled reference")
            .contents();
        assert_eq!(fs::read(&bundled_reference).unwrap(), expected);
        assert_eq!(fs::read_to_string(&extra).unwrap(), "my notes");
    }

    #[test]
    fn bundled_skills_have_a_project_section() {
        let skills = BUNDLED_TEMPLATES_DIR
            .get_dir(SKILLS_DIRNAME)
            .expect("bundled skills dir");

        for skill in skills.dirs() {
            let skill_md = skill
                .get_file(skill.path().join(SKILL_FILE))
                .expect("bundled SKILL.md")
                .contents_utf8()
                .expect("utf8 SKILL.md");
            assert!(
                skill_md.contains(PROJECT_SECTION_MARKER),
                "{} lacks the project section marker",
                skill.path().display()
            );
        }
    }

    #[test]
    fn sync_syncs_managed_section_of_skill_preserving_project_data() {
        let spex_dir = tempdir().expect("create spex dir");
        scaffold(spex_dir.path(), spex_dir.path()).expect("scaffold");

        let skill = spex_dir.path().join(SKILL);
        fs::write(
            &skill,
            "stale managed content\n\n---\n\n# Project\n\nmy custom notes\n",
        )
        .expect("seed SKILL.md");

        sync_templates(spex_dir.path(), spex_dir.path(), &[]).expect("sync");

        let updated = fs::read_to_string(&skill).expect("read SKILL.md");
        let template = BUNDLED_TEMPLATES_DIR
            .get_file("skills/spex-setup/SKILL.md")
            .expect("bundled skill")
            .contents_utf8()
            .expect("utf8 SKILL.md");
        let (managed, _) = template
            .split_once(PROJECT_SECTION_MARKER)
            .expect("bundled SKILL.md has the project marker");

        assert!(updated.starts_with(managed), "managed section not synced");
        assert!(
            updated.ends_with("# Project\n\nmy custom notes\n"),
            "project-specific section not preserved"
        );
    }

    #[test]
    fn sync_preserves_skill_opted_out_of_sync_with_upstream() {
        let spex_dir = tempdir().expect("create spex dir");
        scaffold(spex_dir.path(), spex_dir.path()).expect("scaffold");

        let skill = spex_dir.path().join(SKILL);
        fs::write(
            &skill,
            "---\nname: spex-setup\ndescription: customized\nmetadata:\n  sync-with-upstream: false\n---\ncustomized body\n",
        )
        .expect("opt skill out of sync");

        sync_templates(spex_dir.path(), spex_dir.path(), &[]).expect("sync");

        assert_eq!(
            fs::read_to_string(&skill).unwrap(),
            "---\nname: spex-setup\ndescription: customized\nmetadata:\n  sync-with-upstream: false\n---\ncustomized body\n"
        );
    }

    #[test]
    fn sync_overwrites_skill_without_explicit_opt_out() {
        let spex_dir = tempdir().expect("create spex dir");
        scaffold(spex_dir.path(), spex_dir.path()).expect("scaffold");

        let skill = spex_dir.path().join(SKILL);
        fs::write(
            &skill,
            "---\nname: spex-setup\ndescription: tampered\n---\ntampered\n",
        )
        .expect("tamper skill");

        sync_templates(spex_dir.path(), spex_dir.path(), &[]).expect("sync");

        let expected = BUNDLED_TEMPLATES_DIR
            .get_file("skills/spex-setup/SKILL.md")
            .expect("bundled skill")
            .contents();
        assert_eq!(fs::read(&skill).expect("read skill"), expected);
    }
}