diff --git a/CLAUDE.md b/CLAUDE.md index 7aec3dd..46e267e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -32,6 +32,7 @@ ai-config-kit/ │ ├── winsurf.py # Windsurf (.windsurf/rules/*.md) │ ├── codex.py # OpenAI Codex CLI (AGENTS.md sections) │ ├── copilot.py # GitHub Copilot (.github/instructions/*.md) +│ ├── anteroom.py # Anteroom (ANTEROOM.md sections) │ └── detector.py # Tool detection logic ├── cli/ # Typer CLI commands │ ├── main.py # CLI app definition @@ -397,6 +398,7 @@ Different IDEs support different component types: - **Claude Code**: All components (instructions, MCP, hooks, commands, resources) - **Cline**: Instructions and resources only - **Codex CLI**: Instructions and resources only (via AGENTS.md sections) +- **Anteroom**: Instructions and resources only (via ANTEROOM.md sections) - **Cursor**: Instructions and resources only - **Kiro**: Instructions and resources only - **Roo Code**: Instructions, MCP, commands, and resources @@ -410,6 +412,7 @@ Components are translated to IDE-specific formats: - **Claude Code**: `.md` files in `.claude/rules/`, `.claude/hooks/`, `.claude/commands/` - **Cline**: `.md` files in `.clinerules/` - **Codex CLI**: Sections in `AGENTS.md` at project root (using HTML comment markers) +- **Anteroom**: Sections in `ANTEROOM.md` at project root (using HTML comment markers) - **Cursor**: `.mdc` files in `.cursor/rules/` - **Roo Code**: `.md` files in `.roo/rules/`, `.roo/commands/` - **Kiro**: `.md` files in `.kiro/steering/` diff --git a/README.md b/README.md index bb505d3..7fbc4ee 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ devsync install - **Instructions** -- share coding standards, style guides, and AI prompts from Git repos - **MCP Servers** -- distribute Model Context Protocol configs with secure credential management - **Packages** -- bundle instructions, MCP servers, hooks, commands, and resources together -- **22 IDE integrations** -- Claude Code, Cursor, Windsurf, GitHub Copilot, and 18 more +- **23 IDE integrations** -- Claude Code, Cursor, Windsurf, GitHub Copilot, and 19 more - **Templates** -- IDE-targeted content with slash commands, hooks, and backups - **Conflict resolution** -- skip, overwrite, or rename when files already exist diff --git a/devsync/ai_tools/anteroom.py b/devsync/ai_tools/anteroom.py new file mode 100644 index 0000000..cc2540b --- /dev/null +++ b/devsync/ai_tools/anteroom.py @@ -0,0 +1,216 @@ +"""Anteroom AI tool integration.""" + +import re +import shutil +from pathlib import Path +from typing import Optional + +from devsync.ai_tools.base import AITool +from devsync.core.models import AIToolType, InstallationScope, Instruction + +START_MARKER = "" +END_MARKER = "" +SECTION_PATTERN = r"\n.*?\n" + + +class AnteroomTool(AITool): + """Integration for Anteroom. + + Anteroom reads a single ANTEROOM.md file at the project root. + DevSync manages individual instruction sections using HTML comment markers: + + + ... instruction content ... + + """ + + @property + def tool_type(self) -> AIToolType: + """Return the AI tool type identifier.""" + return AIToolType.ANTEROOM + + @property + def tool_name(self) -> str: + """Return human-readable tool name.""" + return "Anteroom" + + def is_installed(self) -> bool: + """Check if Anteroom is installed on the system. + + Returns: + True if aroom binary is found on PATH or ~/.anteroom/ exists + """ + if shutil.which("aroom") is not None: + return True + config_dir = Path.home() / ".anteroom" + return config_dir.exists() + + def get_instructions_directory(self) -> Path: + """Get the directory where instructions should be installed. + + Raises: + NotImplementedError: Anteroom only supports project-level installation + """ + raise NotImplementedError( + f"{self.tool_name} global installation is not supported. " + "Anteroom uses project-level ANTEROOM.md only. " + "Please use project-level installation instead (--scope project)." + ) + + def get_instruction_file_extension(self) -> str: + """Get the file extension for Anteroom instructions. + + Returns: + File extension including the dot + """ + return ".md" + + def get_project_instructions_directory(self, project_root: Path) -> Path: + """Get the directory for project-specific Anteroom instructions. + + ANTEROOM.md lives at the project root. + + Args: + project_root: Path to the project root directory + + Returns: + Path to project root + """ + return project_root + + def get_instruction_path( + self, + instruction_name: str, + scope: InstallationScope = InstallationScope.GLOBAL, + project_root: Optional[Path] = None, + ) -> Path: + """Get the path to ANTEROOM.md. + + Args: + instruction_name: Name of the instruction (unused for path) + scope: Installation scope (must be PROJECT) + project_root: Project root path + + Returns: + Path to ANTEROOM.md + + Raises: + ValueError: If scope is PROJECT but project_root is None + NotImplementedError: If scope is GLOBAL + """ + if scope == InstallationScope.GLOBAL: + raise NotImplementedError( + f"{self.tool_name} global installation is not supported. " + "Please use project-level installation instead (--scope project)." + ) + if project_root is None: + raise ValueError("project_root is required for PROJECT scope") + return project_root / "ANTEROOM.md" + + def instruction_exists( + self, + instruction_name: str, + scope: InstallationScope = InstallationScope.GLOBAL, + project_root: Optional[Path] = None, + ) -> bool: + """Check if an instruction section exists in ANTEROOM.md. + + Args: + instruction_name: Name of the instruction + scope: Installation scope + project_root: Project root path + + Returns: + True if the instruction's section markers exist in ANTEROOM.md + """ + try: + path = self.get_instruction_path(instruction_name, scope, project_root) + if not path.exists(): + return False + content = path.read_text(encoding="utf-8") + start = START_MARKER.format(name=instruction_name) + return start in content + except (FileNotFoundError, ValueError, NotImplementedError): + return False + + def install_instruction( + self, + instruction: Instruction, + overwrite: bool = False, + scope: InstallationScope = InstallationScope.GLOBAL, + project_root: Optional[Path] = None, + ) -> Path: + """Install an instruction as a section in ANTEROOM.md. + + Args: + instruction: Instruction to install + overwrite: Whether to overwrite existing section + scope: Installation scope + project_root: Project root path + + Returns: + Path to ANTEROOM.md + + Raises: + FileExistsError: If instruction section exists and overwrite=False + """ + path = self.get_instruction_path(instruction.name, scope, project_root) + + start = START_MARKER.format(name=instruction.name) + end = END_MARKER.format(name=instruction.name) + section = f"{start}\n{instruction.content}\n{end}" + + if path.exists(): + content = path.read_text(encoding="utf-8") + if start in content: + if not overwrite: + raise FileExistsError(f"Instruction already exists in ANTEROOM.md: {instruction.name}") + pattern = SECTION_PATTERN.format(name=re.escape(instruction.name)) + content = re.sub(pattern, section, content, flags=re.DOTALL) + path.write_text(content, encoding="utf-8") + return path + if content and not content.endswith("\n"): + content += "\n" + content += "\n" + section + "\n" + path.write_text(content, encoding="utf-8") + else: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(section + "\n", encoding="utf-8") + + return path + + def uninstall_instruction( + self, + instruction_name: str, + scope: InstallationScope = InstallationScope.GLOBAL, + project_root: Optional[Path] = None, + ) -> bool: + """Remove an instruction section from ANTEROOM.md. + + Args: + instruction_name: Name of instruction to remove + scope: Installation scope + project_root: Project root path + + Returns: + True if section was removed, False if it didn't exist + """ + try: + path = self.get_instruction_path(instruction_name, scope, project_root) + if not path.exists(): + return False + + content = path.read_text(encoding="utf-8") + start = START_MARKER.format(name=instruction_name) + if start not in content: + return False + + pattern = SECTION_PATTERN.format(name=re.escape(instruction_name)) + new_content = re.sub(pattern, "", content, flags=re.DOTALL) + new_content = re.sub(r"\n{3,}", "\n\n", new_content).strip() + if new_content: + new_content += "\n" + path.write_text(new_content, encoding="utf-8") + return True + except (FileNotFoundError, ValueError, NotImplementedError): + return False diff --git a/devsync/ai_tools/capability_registry.py b/devsync/ai_tools/capability_registry.py index c079672..9505569 100644 --- a/devsync/ai_tools/capability_registry.py +++ b/devsync/ai_tools/capability_registry.py @@ -461,6 +461,24 @@ def supports_component(self, component_type: ComponentType) -> bool: "DevSync manages sections within this file using HTML comment markers." ), ), + AIToolType.ANTEROOM: IDECapability( + tool_type=AIToolType.ANTEROOM, + tool_name="Anteroom", + supported_components={ + ComponentType.INSTRUCTION, + ComponentType.RESOURCE, + }, + instructions_directory="", + instruction_file_extension=".md", + supports_project_scope=True, + supports_global_scope=False, + mcp_config_path=None, + mcp_project_config_path=None, + notes=( + "Anteroom uses a single ANTEROOM.md file at the project root. " + "DevSync manages sections within this file using HTML comment markers." + ), + ), AIToolType.COPILOT: IDECapability( tool_type=AIToolType.COPILOT, tool_name="GitHub Copilot", diff --git a/devsync/ai_tools/detector.py b/devsync/ai_tools/detector.py index d296dc3..eab2369 100644 --- a/devsync/ai_tools/detector.py +++ b/devsync/ai_tools/detector.py @@ -5,6 +5,7 @@ from devsync.ai_tools.aider import AiderTool from devsync.ai_tools.amazonq import AmazonQTool from devsync.ai_tools.amp import AmpTool +from devsync.ai_tools.anteroom import AnteroomTool from devsync.ai_tools.antigravity import AntigravityTool from devsync.ai_tools.augment import AugmentTool from devsync.ai_tools.base import AITool @@ -56,6 +57,7 @@ def __init__(self) -> None: AIToolType.OPENHANDS: OpenHandsTool(), AIToolType.AMP: AmpTool(), AIToolType.OPENCODE: OpenCodeTool(), + AIToolType.ANTEROOM: AnteroomTool(), } def detect_installed_tools(self) -> list[AITool]: @@ -132,6 +134,7 @@ def get_primary_tool(self) -> Optional[AITool]: AIToolType.OPENHANDS, AIToolType.AMP, AIToolType.OPENCODE, + AIToolType.ANTEROOM, ] for tool_type in priority: diff --git a/devsync/ai_tools/translator.py b/devsync/ai_tools/translator.py index 2816ea2..8e27454 100644 --- a/devsync/ai_tools/translator.py +++ b/devsync/ai_tools/translator.py @@ -823,6 +823,34 @@ def translate_mcp_server(self, component: MCPServerComponent, package_root: Path raise NotImplementedError("OpenCode does not support MCP servers") +class AnteroomTranslator(ComponentTranslator): + """Translator for Anteroom (ANTEROOM.md at project root).""" + + @property + def tool_type(self) -> AIToolType: + return AIToolType.ANTEROOM + + def translate_instruction(self, component: InstructionComponent, package_root: Path) -> TranslatedComponent: + """Translate instruction to Anteroom format with section markers.""" + instruction_path = package_root / component.file + with open(instruction_path, "r") as f: + content = f.read() + + wrapped = f"\n{content}\n" + + return TranslatedComponent( + component_type=ComponentType.INSTRUCTION, + component_name=component.name, + target_path="ANTEROOM.md", + content=wrapped, + metadata={"section_based": True}, + ) + + def translate_mcp_server(self, component: MCPServerComponent, package_root: Path) -> TranslatedComponent: + """Anteroom does not support MCP servers.""" + raise NotImplementedError("Anteroom does not support MCP servers") + + class CopilotTranslator(ComponentTranslator): """Translator for GitHub Copilot (.github/instructions/).""" @@ -899,6 +927,7 @@ def get_translator(tool_type: AIToolType) -> ComponentTranslator: AIToolType.OPENHANDS: OpenHandsTranslator, AIToolType.AMP: AmpTranslator, AIToolType.OPENCODE: OpenCodeTranslator, + AIToolType.ANTEROOM: AnteroomTranslator, } translator_class = translators.get(tool_type) diff --git a/devsync/core/component_detector.py b/devsync/core/component_detector.py index 8c2d151..a1a3dc7 100644 --- a/devsync/core/component_detector.py +++ b/devsync/core/component_detector.py @@ -242,6 +242,7 @@ class ComponentDetector: SINGLE_INSTRUCTION_FILES = { ".github/copilot-instructions.md": "copilot", "AGENTS.md": "codex", + "ANTEROOM.md": "anteroom", } INSTRUCTION_EXTENSIONS = {".md", ".mdc", ".instructions.md"} diff --git a/devsync/core/models.py b/devsync/core/models.py index 326e0cc..b65b947 100644 --- a/devsync/core/models.py +++ b/devsync/core/models.py @@ -31,6 +31,7 @@ class AIToolType(Enum): OPENHANDS = "openhands" AMP = "amp" OPENCODE = "opencode" + ANTEROOM = "anteroom" class ConflictResolution(Enum): @@ -455,6 +456,7 @@ def __post_init__(self) -> None: "openhands", "amp", "opencode", + "anteroom", ] if self.ide not in valid_ides: raise ValueError(f"Invalid IDE type: {self.ide}. Must be one of {valid_ides}") diff --git a/docs/index.md b/docs/index.md index 0986e91..c4723c7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -40,9 +40,9 @@ Bundle instructions, MCP servers, hooks, commands, and resources into installabl
-### 22 IDE Integrations +### 23 IDE Integrations -Claude Code, Cursor, Windsurf, GitHub Copilot, Codex CLI, Cline, Kiro, Roo Code, and 14 more. +Claude Code, Cursor, Windsurf, GitHub Copilot, Codex CLI, Cline, Kiro, Roo Code, Anteroom, and 14 more. [See all IDEs](ide-integrations/index.md){ .md-button } diff --git a/tests/unit/packages/test_capability_registry.py b/tests/unit/packages/test_capability_registry.py index f68bfd2..d0ec20f 100644 --- a/tests/unit/packages/test_capability_registry.py +++ b/tests/unit/packages/test_capability_registry.py @@ -85,6 +85,7 @@ def test_registry_contains_all_tools(self) -> None: assert AIToolType.OPENHANDS in CAPABILITY_REGISTRY assert AIToolType.AMP in CAPABILITY_REGISTRY assert AIToolType.OPENCODE in CAPABILITY_REGISTRY + assert AIToolType.ANTEROOM in CAPABILITY_REGISTRY def test_cursor_capabilities(self) -> None: """Test Cursor IDE capabilities.""" @@ -263,7 +264,7 @@ def test_get_supported_tools_for_instruction(self) -> None: tools = get_supported_tools_for_component(ComponentType.INSTRUCTION) # All tools support instructions - assert len(tools) == 22 + assert len(tools) == 23 assert AIToolType.CURSOR in tools assert AIToolType.CLAUDE in tools assert AIToolType.WINSURF in tools @@ -286,6 +287,7 @@ def test_get_supported_tools_for_instruction(self) -> None: assert AIToolType.OPENHANDS in tools assert AIToolType.AMP in tools assert AIToolType.OPENCODE in tools + assert AIToolType.ANTEROOM in tools def test_get_supported_tools_for_mcp_server(self) -> None: """Test getting tools that support MCP servers.""" @@ -330,7 +332,7 @@ def test_get_supported_tools_for_resource(self) -> None: tools = get_supported_tools_for_component(ComponentType.RESOURCE) # All tools except Copilot support resources - assert len(tools) == 21 + assert len(tools) == 22 assert AIToolType.CURSOR in tools assert AIToolType.CLAUDE in tools assert AIToolType.WINSURF in tools @@ -352,6 +354,7 @@ def test_get_supported_tools_for_resource(self) -> None: assert AIToolType.OPENHANDS in tools assert AIToolType.AMP in tools assert AIToolType.OPENCODE in tools + assert AIToolType.ANTEROOM in tools assert AIToolType.COPILOT not in tools # Instructions and MCP only def test_validate_component_support_true(self) -> None: diff --git a/tests/unit/test_ai_tools_anteroom.py b/tests/unit/test_ai_tools_anteroom.py new file mode 100644 index 0000000..33dc12f --- /dev/null +++ b/tests/unit/test_ai_tools_anteroom.py @@ -0,0 +1,245 @@ +"""Tests for Anteroom AI tool integration.""" + +import pytest + +from devsync.ai_tools.anteroom import AnteroomTool +from devsync.core.models import AIToolType, InstallationScope, Instruction + + +@pytest.fixture +def anteroom_tool(): + """Create an Anteroom tool instance.""" + return AnteroomTool() + + +@pytest.fixture +def sample_instruction(): + """Create a sample instruction for testing.""" + return Instruction( + name="test-instruction", + description="Test instruction", + content="# Test Instruction\n\nThis is test content.", + file_path="test.md", + tags=["test"], + ) + + +@pytest.fixture +def second_instruction(): + """Create a second instruction for testing multi-section behavior.""" + return Instruction( + name="second-instruction", + description="Second instruction", + content="# Second Instruction\n\nMore content here.", + file_path="second.md", + tags=["test"], + ) + + +class TestAnteroomTool: + """Test suite for AnteroomTool.""" + + def test_tool_type(self, anteroom_tool: AnteroomTool) -> None: + assert anteroom_tool.tool_type == AIToolType.ANTEROOM + + def test_tool_name(self, anteroom_tool: AnteroomTool) -> None: + assert anteroom_tool.tool_name == "Anteroom" + + def test_is_installed_when_binary_present( + self, anteroom_tool: AnteroomTool, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.setattr("devsync.ai_tools.anteroom.shutil.which", lambda cmd: "/usr/local/bin/aroom") + assert anteroom_tool.is_installed() is True + + def test_is_installed_when_config_dir_exists( + self, + anteroom_tool: AnteroomTool, + monkeypatch: pytest.MonkeyPatch, + tmp_path, # type: ignore[no-untyped-def] + ) -> None: + monkeypatch.setattr("devsync.ai_tools.anteroom.shutil.which", lambda cmd: None) + anteroom_dir = tmp_path / ".anteroom" + anteroom_dir.mkdir() + monkeypatch.setattr("devsync.ai_tools.anteroom.Path.home", lambda: tmp_path) + assert anteroom_tool.is_installed() is True + + def test_is_installed_when_absent( + self, + anteroom_tool: AnteroomTool, + monkeypatch: pytest.MonkeyPatch, + tmp_path, # type: ignore[no-untyped-def] + ) -> None: + monkeypatch.setattr("devsync.ai_tools.anteroom.shutil.which", lambda cmd: None) + monkeypatch.setattr("devsync.ai_tools.anteroom.Path.home", lambda: tmp_path) + assert anteroom_tool.is_installed() is False + + def test_get_instructions_directory_raises_not_implemented(self, anteroom_tool: AnteroomTool) -> None: + with pytest.raises(NotImplementedError) as exc_info: + anteroom_tool.get_instructions_directory() + assert "global installation is not supported" in str(exc_info.value).lower() + + def test_get_instruction_file_extension(self, anteroom_tool: AnteroomTool) -> None: + assert anteroom_tool.get_instruction_file_extension() == ".md" + + def test_get_project_instructions_directory( + self, anteroom_tool: AnteroomTool, temp_dir # type: ignore[no-untyped-def] + ) -> None: + project_root = temp_dir / "project" + project_root.mkdir() + assert anteroom_tool.get_project_instructions_directory(project_root) == project_root + + def test_get_instruction_path(self, anteroom_tool: AnteroomTool, temp_dir) -> None: # type: ignore[no-untyped-def] + project_root = temp_dir / "project" + project_root.mkdir() + path = anteroom_tool.get_instruction_path("test", scope=InstallationScope.PROJECT, project_root=project_root) + assert path == project_root / "ANTEROOM.md" + + def test_get_instruction_path_global_raises(self, anteroom_tool: AnteroomTool) -> None: + with pytest.raises(NotImplementedError): + anteroom_tool.get_instruction_path("test", scope=InstallationScope.GLOBAL) + + def test_get_instruction_path_no_project_root_raises(self, anteroom_tool: AnteroomTool) -> None: + with pytest.raises(ValueError): + anteroom_tool.get_instruction_path("test", scope=InstallationScope.PROJECT, project_root=None) + + def test_install_creates_anteroom_md( + self, anteroom_tool: AnteroomTool, temp_dir, sample_instruction: Instruction # type: ignore[no-untyped-def] + ) -> None: + project_root = temp_dir / "project" + project_root.mkdir() + + path = anteroom_tool.install_instruction( + sample_instruction, scope=InstallationScope.PROJECT, project_root=project_root + ) + + assert path == project_root / "ANTEROOM.md" + assert path.exists() + content = path.read_text(encoding="utf-8") + assert "" in content + assert "" in content + assert "# Test Instruction" in content + + def test_install_appends_to_existing( + self, + anteroom_tool: AnteroomTool, + temp_dir, # type: ignore[no-untyped-def] + sample_instruction: Instruction, + second_instruction: Instruction, + ) -> None: + project_root = temp_dir / "project" + project_root.mkdir() + + anteroom_tool.install_instruction( + sample_instruction, scope=InstallationScope.PROJECT, project_root=project_root + ) + anteroom_tool.install_instruction( + second_instruction, scope=InstallationScope.PROJECT, project_root=project_root + ) + + content = (project_root / "ANTEROOM.md").read_text(encoding="utf-8") + assert "" in content + assert "" in content + assert "# Test Instruction" in content + assert "# Second Instruction" in content + + def test_install_existing_raises_without_overwrite( + self, anteroom_tool: AnteroomTool, temp_dir, sample_instruction: Instruction # type: ignore[no-untyped-def] + ) -> None: + project_root = temp_dir / "project" + project_root.mkdir() + + anteroom_tool.install_instruction( + sample_instruction, scope=InstallationScope.PROJECT, project_root=project_root + ) + + with pytest.raises(FileExistsError): + anteroom_tool.install_instruction( + sample_instruction, scope=InstallationScope.PROJECT, project_root=project_root + ) + + def test_install_overwrite_replaces_section( + self, anteroom_tool: AnteroomTool, temp_dir, sample_instruction: Instruction # type: ignore[no-untyped-def] + ) -> None: + project_root = temp_dir / "project" + project_root.mkdir() + + anteroom_tool.install_instruction( + sample_instruction, scope=InstallationScope.PROJECT, project_root=project_root + ) + + updated = Instruction( + name="test-instruction", + description="Updated", + content="# Updated Content", + file_path="test.md", + ) + anteroom_tool.install_instruction( + updated, overwrite=True, scope=InstallationScope.PROJECT, project_root=project_root + ) + + content = (project_root / "ANTEROOM.md").read_text(encoding="utf-8") + assert "# Updated Content" in content + assert "# Test Instruction" not in content + assert content.count("") == 1 + + def test_instruction_exists_true( + self, anteroom_tool: AnteroomTool, temp_dir, sample_instruction: Instruction # type: ignore[no-untyped-def] + ) -> None: + project_root = temp_dir / "project" + project_root.mkdir() + + anteroom_tool.install_instruction( + sample_instruction, scope=InstallationScope.PROJECT, project_root=project_root + ) + + assert ( + anteroom_tool.instruction_exists( + "test-instruction", scope=InstallationScope.PROJECT, project_root=project_root + ) + is True + ) + + def test_instruction_exists_false_no_file( + self, anteroom_tool: AnteroomTool, temp_dir # type: ignore[no-untyped-def] + ) -> None: + project_root = temp_dir / "project" + project_root.mkdir() + + assert ( + anteroom_tool.instruction_exists("nonexistent", scope=InstallationScope.PROJECT, project_root=project_root) + is False + ) + + def test_uninstall_removes_section( + self, anteroom_tool: AnteroomTool, temp_dir, sample_instruction: Instruction # type: ignore[no-untyped-def] + ) -> None: + project_root = temp_dir / "project" + project_root.mkdir() + + anteroom_tool.install_instruction( + sample_instruction, scope=InstallationScope.PROJECT, project_root=project_root + ) + + result = anteroom_tool.uninstall_instruction( + "test-instruction", scope=InstallationScope.PROJECT, project_root=project_root + ) + + assert result is True + content = (project_root / "ANTEROOM.md").read_text(encoding="utf-8") + assert "" not in content + + def test_uninstall_nonexistent_returns_false( + self, anteroom_tool: AnteroomTool, temp_dir # type: ignore[no-untyped-def] + ) -> None: + project_root = temp_dir / "project" + project_root.mkdir() + + result = anteroom_tool.uninstall_instruction( + "nonexistent", scope=InstallationScope.PROJECT, project_root=project_root + ) + assert result is False + + def test_repr(self, anteroom_tool: AnteroomTool) -> None: + repr_str = repr(anteroom_tool) + assert "AnteroomTool" in repr_str + assert AIToolType.ANTEROOM.value in repr_str diff --git a/tests/unit/test_ai_tools_detector.py b/tests/unit/test_ai_tools_detector.py index 79ca143..ac99e74 100644 --- a/tests/unit/test_ai_tools_detector.py +++ b/tests/unit/test_ai_tools_detector.py @@ -144,7 +144,7 @@ class TestAIToolDetector: def test_init_creates_all_tools(self, detector): """Test that detector initializes with all supported tools.""" - assert len(detector.tools) == 22 + assert len(detector.tools) == 23 assert AIToolType.CURSOR in detector.tools assert AIToolType.COPILOT in detector.tools assert AIToolType.WINSURF in detector.tools @@ -205,7 +205,7 @@ def test_detect_installed_tools_all(self, mock_all_tools_installed): # Create fresh detector with mocked paths detector = AIToolDetector() installed = detector.detect_installed_tools() - assert len(installed) == 22 + assert len(installed) == 23 def test_get_tool_by_name_valid(self, detector): """Test get_tool_by_name with valid tool name.""" @@ -336,7 +336,7 @@ def test_is_any_tool_installed_false(self, temp_dir, monkeypatch): def test_get_tool_names(self, detector): """Test get_tool_names returns all tool names.""" names = detector.get_tool_names() - assert len(names) == 22 + assert len(names) == 23 assert "cursor" in names assert "copilot" in names assert "winsurf" in names @@ -373,7 +373,7 @@ def test_get_detection_summary(self, mock_all_tools_installed): """Test get_detection_summary.""" detector = AIToolDetector() summary = detector.get_detection_summary() - assert len(summary) == 22 + assert len(summary) == 23 assert all(isinstance(v, bool) for v in summary.values()) def test_format_detection_summary(self, mock_all_tools_installed):