main-spex 0.20.0

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

    const USER_SKILL: &str = ".claude/skills/my-skill/SKILL.md";

    fn bundled_agents_md() -> String {
        let bytes = BUNDLED_TEMPLATES_DIR
            .get_file(AGENTS_FILENAME)
            .expect("bundled AGENTS.md")
            .contents();
        String::from_utf8(bytes.to_vec()).expect("utf8 AGENTS.md")
    }

    fn write(root: &Path, relative: &str, contents: &str) {
        let path = root.join(relative);
        fs::create_dir_all(path.parent().expect("parent")).expect("create parent dirs");
        fs::write(path, contents).expect("write file");
    }

    #[test]
    fn verify_passes_in_an_empty_agent_root() {
        let root = tempdir().expect("create agent root");

        assert!(verify_agent_files(root.path()).is_ok());
    }

    #[test]
    fn verify_fails_when_agents_md_and_claude_md_are_both_regular_files() {
        let root = tempdir().expect("create agent root");
        write(root.path(), "AGENTS.md", "agents");
        write(root.path(), "CLAUDE.md", "claude");

        assert!(verify_agent_files(root.path()).is_err());
    }

    #[test]
    fn verify_fails_when_a_claude_skill_has_a_bundled_name() {
        let root = tempdir().expect("create agent root");
        write(root.path(), ".claude/skills/spex-spec/SKILL.md", "mine");

        assert!(verify_agent_files(root.path()).is_err());
    }

    #[test]
    fn verify_fails_when_a_claude_skill_clashes_with_an_agents_skill() {
        let root = tempdir().expect("create agent root");
        write(root.path(), USER_SKILL, "from claude");
        write(
            root.path(),
            ".agents/skills/my-skill/SKILL.md",
            "from agents",
        );

        assert!(verify_agent_files(root.path()).is_err());
    }

    #[test]
    fn write_agents_md_writes_the_template_when_nothing_exists() {
        let root = tempdir().expect("create agent root");

        write_agents_md(root.path()).expect("write AGENTS.md");

        assert_eq!(
            fs::read_to_string(root.path().join(AGENTS_FILENAME)).expect("read AGENTS.md"),
            bundled_agents_md()
        );
    }

    #[test]
    fn write_agents_md_keeps_an_existing_agents_md_below_the_marker() {
        let root = tempdir().expect("create agent root");
        write(root.path(), "AGENTS.md", "# My package\n\nuse tabs\n");

        write_agents_md(root.path()).expect("write AGENTS.md");

        let written = fs::read_to_string(root.path().join(AGENTS_FILENAME)).expect("read");
        let template = bundled_agents_md();
        let (managed, _) = template
            .split_once("\n---\n\n# Project")
            .expect("bundled AGENTS.md has the project marker");
        assert!(written.starts_with(managed), "managed section not on top");
        assert!(
            written.ends_with("# My package\n\nuse tabs\n"),
            "existing content not kept"
        );
        assert_eq!(written.matches("# Project").count(), 1);
    }

    #[cfg(unix)]
    #[test]
    fn write_agents_md_links_claude_md_to_agents_md() {
        let root = tempdir().expect("create agent root");

        write_agents_md(root.path()).expect("write AGENTS.md");

        assert_eq!(
            fs::read_link(root.path().join(CLAUDE_FILENAME)).expect("read symlink"),
            Path::new(AGENTS_FILENAME)
        );
    }

    #[cfg(unix)]
    #[test]
    fn write_agents_md_moves_an_existing_claude_md_into_agents_md() {
        let root = tempdir().expect("create agent root");
        write(root.path(), "CLAUDE.md", "# Claude notes\n");

        write_agents_md(root.path()).expect("write AGENTS.md");

        let written = fs::read_to_string(root.path().join(AGENTS_FILENAME)).expect("read");
        assert!(written.ends_with("# Claude notes\n"));
        assert_eq!(
            fs::read_link(root.path().join(CLAUDE_FILENAME)).expect("read symlink"),
            Path::new(AGENTS_FILENAME)
        );
    }

    #[cfg(unix)]
    #[test]
    fn link_claude_dir_links_the_whole_directory_when_absent() {
        let root = tempdir().expect("create agent root");
        fs::create_dir_all(root.path().join(".agents/skills")).expect("create skills");

        link_claude_dir(root.path()).expect("link");

        assert_eq!(
            fs::read_link(root.path().join(CLAUDE_DIRNAME)).expect("read symlink"),
            Path::new(AGENTS_DIRNAME)
        );
    }

    #[cfg(unix)]
    #[test]
    fn link_claude_dir_links_only_skills_into_an_existing_claude_directory() {
        let root = tempdir().expect("create agent root");
        write(root.path(), ".claude/settings.json", "{}");
        fs::create_dir_all(root.path().join(".agents/skills")).expect("create skills");

        link_claude_dir(root.path()).expect("link");

        assert!(root.path().join(".claude/settings.json").is_file());
        assert_eq!(
            fs::read_link(root.path().join(".claude/skills")).expect("read symlink"),
            Path::new("../.agents/skills")
        );
    }

    #[cfg(unix)]
    #[test]
    fn link_claude_dir_leaves_an_existing_link_alone() {
        let root = tempdir().expect("create agent root");
        fs::create_dir_all(root.path().join("elsewhere")).expect("create target");
        std::os::unix::fs::symlink("elsewhere", root.path().join(CLAUDE_DIRNAME))
            .expect("create symlink");

        link_claude_dir(root.path()).expect("link");

        assert_eq!(
            fs::read_link(root.path().join(CLAUDE_DIRNAME)).expect("read symlink"),
            Path::new("elsewhere")
        );
    }

    #[cfg(unix)]
    #[test]
    fn move_claude_skills_moves_them_into_agents_and_leaves_a_skills_link() {
        let root = tempdir().expect("create agent root");
        write(root.path(), USER_SKILL, "mine");
        write(root.path(), ".claude/settings.json", "{}");

        move_claude_skills(root.path()).expect("move");
        link_claude_dir(root.path()).expect("link");

        assert_eq!(
            fs::read_to_string(root.path().join(".agents/skills/my-skill/SKILL.md"))
                .expect("read moved skill"),
            "mine"
        );
        assert!(root.path().join(".claude/settings.json").is_file());
        assert!(
            root.path().join(USER_SKILL).is_file(),
            "the moved skill should resolve through the `.claude/skills` link"
        );
        assert!(
            fs::read_link(root.path().join(".claude/skills")).is_ok(),
            "`.claude/skills` should be a symlink"
        );
    }

    #[test]
    fn move_claude_skills_does_nothing_without_claude_skills() {
        let root = tempdir().expect("create agent root");

        move_claude_skills(root.path()).expect("move");

        assert!(!root.path().join(".agents").exists());
    }
}