"""Tests for ClaudeIntegration.""" import codecs import json import os from unittest.mock import patch import yaml from specify_cli.integrations import INTEGRATION_REGISTRY, get_integration from specify_cli.integrations.base import IntegrationBase from specify_cli.integrations.claude import ARGUMENT_HINTS from specify_cli.integrations.manifest import IntegrationManifest class TestClaudeIntegration: def test_registered(self): assert "claude" in INTEGRATION_REGISTRY assert get_integration("claude") is not None def test_is_base_integration(self): assert isinstance(get_integration("claude"), IntegrationBase) def test_config_uses_skills(self): integration = get_integration("claude") assert integration.config["folder"] == ".claude/" assert integration.config["commands_subdir"] == "skills" def test_registrar_config_uses_skill_layout(self): integration = get_integration("claude") assert integration.registrar_config["dir"] == ".claude/skills" assert integration.registrar_config["format"] == "markdown" assert integration.registrar_config["args"] == "$ARGUMENTS" assert integration.registrar_config["extension"] == "/SKILL.md" def test_context_file(self): integration = get_integration("claude") assert integration.context_file == "CLAUDE.md" def test_setup_creates_skill_files(self, tmp_path): integration = get_integration("claude") manifest = IntegrationManifest("claude", tmp_path) created = integration.setup(tmp_path, manifest, script_type="sh") skill_files = [path for path in created if path.name == "SKILL.md"] assert skill_files skills_dir = tmp_path / ".claude" / "skills" assert skills_dir.is_dir() plan_skill = skills_dir / "speckit-plan" / "SKILL.md" assert plan_skill.exists() content = plan_skill.read_text(encoding="utf-8") assert "{SCRIPT}" not in content assert "{ARGS}" not in content assert "__AGENT__" not in content assert "__SPECKIT_COMMAND_" not in content, "unprocessed __SPECKIT_COMMAND_*__" assert "/speckit." not in content, "skills agent must use /speckit- not /speckit." parts = content.split("---", 2) parsed = yaml.safe_load(parts[1]) assert parsed["name"] == "speckit-plan" assert parsed["user-invocable"] is True assert parsed["disable-model-invocation"] is False assert parsed["metadata"]["source"] == "templates/commands/plan.md" def test_setup_upserts_context_section(self, tmp_path): integration = get_integration("claude") manifest = IntegrationManifest("claude", tmp_path) integration.setup(tmp_path, manifest, script_type="sh") ctx_path = tmp_path / integration.context_file assert ctx_path.exists() content = ctx_path.read_text(encoding="utf-8") assert "" in content assert "" in content assert "read the current plan" in content def test_upsert_context_section_strips_bom(self, tmp_path): """Existing context file with UTF-8 BOM must be cleaned up on upsert.""" integration = get_integration("claude") ctx_path = tmp_path / integration.context_file # Write a file that starts with a UTF-8 BOM (as the old PowerShell script did) bom = codecs.BOM_UTF8 ctx_path.write_bytes(bom + b"# CLAUDE.md\n\nSome existing content.\n") integration.upsert_context_section(tmp_path) result = ctx_path.read_bytes() assert not result.startswith(bom), "BOM must be stripped after upsert" content = result.decode("utf-8") assert "" in content assert "Some existing content." in content def test_remove_context_section_strips_bom(self, tmp_path): """remove_context_section must clean BOM from context file on Windows-authored files.""" integration = get_integration("claude") ctx_path = tmp_path / integration.context_file marker_content = ( "# CLAUDE.md\n\n" "\n" "For additional context about technologies to be used, project structure,\n" "shell commands, and other important information, read the current plan\n" "\n" ) ctx_path.write_bytes(codecs.BOM_UTF8 + marker_content.encode("utf-8")) result = integration.remove_context_section(tmp_path) assert result is True assert ctx_path.exists(), "File should exist (non-empty content remains)" remaining = ctx_path.read_bytes() assert not remaining.startswith(codecs.BOM_UTF8), "BOM must be stripped after remove" assert b"