Navegador
feat: editor integrations — MCP config generation for Claude Code, Cursor, Codex, Windsurf CLI: navegador editor setup <editor> [--write]. Generates correct MCP config snippets for each AI editor, with optional file write. Closes #27
Commit
8b1274310a7a023d727b774738858d77f5cb5e88e6c82877d2e4c2df80d767e8
Parent
57ba118128bc1f7…
2 files changed
+39
+218
+39
| --- a/navegador/editor.py | ||
| +++ b/navegador/editor.py | ||
| @@ -0,0 +1,39 @@ | ||
| 1 | +""" | |
| 2 | +Editor integrations — generate MCP config snippets for AI coding editors. | |
| 3 | + | |
| 4 | +Supported editors: | |
| 5 | + claude-code — .claude/mcp.json | |
| 6 | + cursor — .cursor/mcp.json | |
| 7 | + codex — .codex/config.json | |
| 8 | + windsurf — .windsurf/mcp.json | |
| 9 | +""" | |
| 10 | + | |
| 11 | +from __future__ import annotations | |
| 12 | + | |
| 13 | +import json | |
| 14 | +from pathlib import Path | |
| 15 | + | |
| 16 | +SUPPORTED_EDITORS = ["claude-code", "cursor", "codex", "windsurf"] | |
| 17 | + | |
| 18 | +# Config file path relative to the project root for each editor | |
| 19 | +_CONFIG_PATHS: dict[str, str] = { | |
| 20 | + "claude-code": ".claude/mcp.json", | |
| 21 | + "cursor": ".cursor/mcp.json", | |
| 22 | + "codex": ".codex/config.json", | |
| 23 | + "windsurf": ".windsurf/mcp.json", | |
| 24 | +} | |
| 25 | + | |
| 26 | + | |
| 27 | +def _mcp_block(db: str) -> dict: | |
| 28 | + """Return the shared mcpServers block used by all editors.""" | |
| 29 | + return { | |
| 30 | + "mcpServers": { | |
| 31 | + "navegador": { | |
| 32 | + "command": "navegador", | |
| 33 | + "args": ["mcp", "--db", db], | |
| 34 | + } | |
| 35 | + } | |
| 36 | + } | |
| 37 | + | |
| 38 | + | |
| 39 | +cla |
| --- a/navegador/editor.py | |
| +++ b/navegador/editor.py | |
| @@ -0,0 +1,39 @@ | |
| --- a/navegador/editor.py | |
| +++ b/navegador/editor.py | |
| @@ -0,0 +1,39 @@ | |
| 1 | """ |
| 2 | Editor integrations — generate MCP config snippets for AI coding editors. |
| 3 | |
| 4 | Supported editors: |
| 5 | claude-code — .claude/mcp.json |
| 6 | cursor — .cursor/mcp.json |
| 7 | codex — .codex/config.json |
| 8 | windsurf — .windsurf/mcp.json |
| 9 | """ |
| 10 | |
| 11 | from __future__ import annotations |
| 12 | |
| 13 | import json |
| 14 | from pathlib import Path |
| 15 | |
| 16 | SUPPORTED_EDITORS = ["claude-code", "cursor", "codex", "windsurf"] |
| 17 | |
| 18 | # Config file path relative to the project root for each editor |
| 19 | _CONFIG_PATHS: dict[str, str] = { |
| 20 | "claude-code": ".claude/mcp.json", |
| 21 | "cursor": ".cursor/mcp.json", |
| 22 | "codex": ".codex/config.json", |
| 23 | "windsurf": ".windsurf/mcp.json", |
| 24 | } |
| 25 | |
| 26 | |
| 27 | def _mcp_block(db: str) -> dict: |
| 28 | """Return the shared mcpServers block used by all editors.""" |
| 29 | return { |
| 30 | "mcpServers": { |
| 31 | "navegador": { |
| 32 | "command": "navegador", |
| 33 | "args": ["mcp", "--db", db], |
| 34 | } |
| 35 | } |
| 36 | } |
| 37 | |
| 38 | |
| 39 | cla |
+218
| --- a/tests/test_editor.py | ||
| +++ b/tests/test_editor.py | ||
| @@ -0,0 +1,218 @@ | ||
| 1 | +"""Tests for editor integration — EditorIntegration class and CLI command.""" | |
| 2 | + | |
| 3 | +import json | |
| 4 | +from pathlib import Path | |
| 5 | +from unittest.mock import MagicMock, patch | |
| 6 | + | |
| 7 | +import pytest | |
| 8 | +from click.testing import CliRunner | |
| 9 | + | |
| 10 | +from navegador.cli.commands import main | |
| 11 | +from navegador.editor import SUPPORTED_EDITORS, EditorIntegration | |
| 12 | + | |
| 13 | +# ── EditorIntegration unit tests ────────────────────────────────────────────── | |
| 14 | + | |
| 15 | + | |
| 16 | +class TestEditorIntegration: | |
| 17 | + def setup_method(self): | |
| 18 | + self.integration = EditorIntegration(db=".navegador/graph.db") | |
| 19 | + | |
| 20 | + # config_for | |
| 21 | + | |
| 22 | + def test_config_for_claude_code(self): | |
| 23 | + cfg = self.integration.config_for("claude-code") | |
| 24 | + assert cfg["mcpServers"]["navegador"]["command"] == "navegador" | |
| 25 | + assert cfg["mcpServers"]["navegador"]["args"] == ["mcp", "--db", ".navegador/graph.db"] | |
| 26 | + | |
| 27 | + def test_config_for_cursor(self): | |
| 28 | + cfg = self.integration.config_for("cursor") | |
| 29 | + assert cfg["mcpServers"]["navegador"]["command"] == "navegador" | |
| 30 | + assert cfg["mcpServers"]["navegador"]["args"] == ["mcp", "--db", ".navegador/graph.db"] | |
| 31 | + | |
| 32 | + def test_config_for_codex(self): | |
| 33 | + cfg = self.integration.config_for("codex") | |
| 34 | + assert cfg["mcpServers"]["navegador"]["command"] == "navegador" | |
| 35 | + assert cfg["mcpServers"]["navegador"]["args"] == ["mcp", "--db", ".navegador/graph.db"] | |
| 36 | + | |
| 37 | + def test_config_for_windsurf(self): | |
| 38 | + cfg = self.integration.config_for("windsurf") | |
| 39 | + assert cfg["mcpServers"]["navegador"]["command"] == "navegador" | |
| 40 | + assert cfg["mcpServers"]["navegador"]["args"] == ["mcp", "--db", ".navegador/graph.db"] | |
| 41 | + | |
| 42 | + def test_config_for_invalid_editor_raises(self): | |
| 43 | + with pytest.raises(ValueError, match="Unsupported editor"): | |
| 44 | + self.integration.config_for("vscode") | |
| 45 | + | |
| 46 | + # custom db path | |
| 47 | + | |
| 48 | + def test_custom_db_path_reflected_in_config(self): | |
| 49 | + integration = EditorIntegration(db="/custom/path/graph.db") | |
| 50 | + cfg = integration.config_for("cursor") | |
| 51 | + assert cfg["mcpServers"]["navegador"]["args"][2] == "/custom/path/graph.db" | |
| 52 | + | |
| 53 | + # config_json | |
| 54 | + | |
| 55 | + def test_config_json_is_valid_json(self): | |
| 56 | + raw = self.integration.config_json("claude-code") | |
| 57 | + parsed = json.loads(raw) | |
| 58 | + assert "mcpServers" in parsed | |
| 59 | + | |
| 60 | + def test_config_json_is_pretty_printed(self): | |
| 61 | + raw = self.integration.config_json("cursor") | |
| 62 | + assert "\n" in raw # indented | |
| 63 | + | |
| 64 | + # config_path | |
| 65 | + | |
| 66 | + def test_config_path_claude_code(self): | |
| 67 | + assert self.integration.config_path("claude-code") == ".claude/mcp.json" | |
| 68 | + | |
| 69 | + def test_config_path_cursor(self): | |
| 70 | + assert self.integration.config_path("cursor") == ".cursor/mcp.json" | |
| 71 | + | |
| 72 | + def test_config_path_codex(self): | |
| 73 | + assert self.integration.config_path("codex") == ".codex/config.json" | |
| 74 | + | |
| 75 | + def test_config_path_windsurf(self): | |
| 76 | + assert self.integration.config_path("windsurf") == ".windsurf/mcp.json" | |
| 77 | + | |
| 78 | + def test_config_path_invalid_editor_raises(self): | |
| 79 | + with pytest.raises(ValueError, match="Unsupported editor"): | |
| 80 | + self.integration.config_path("sublime") | |
| 81 | + | |
| 82 | + # write_config | |
| 83 | + | |
| 84 | + def test_write_config_creates_file(self, tmp_path): | |
| 85 | + written = self.integration.write_config("claude-code", base_dir=str(tmp_path)) | |
| 86 | + assert written.exists() | |
| 87 | + parsed = json.loads(written.read_text()) | |
| 88 | + assert "mcpServers" in parsed | |
| 89 | + | |
| 90 | + def test_write_config_creates_parent_dirs(self, tmp_path): | |
| 91 | + written = self.integration.write_config("windsurf", base_dir=str(tmp_path)) | |
| 92 | + assert (tmp_path / ".windsurf").is_dir() | |
| 93 | + assert written.name == "mcp.json" | |
| 94 | + | |
| 95 | + def test_write_config_returns_path_object(self, tmp_path): | |
| 96 | + result = self.integration.write_config("cursor", base_dir=str(tmp_path)) | |
| 97 | + assert isinstance(result, Path) | |
| 98 | + | |
| 99 | + def test_write_config_content_matches_config_json(self, tmp_path): | |
| 100 | + written = self.integration.write_config("codex", base_dir=str(tmp_path)) | |
| 101 | + assert written.read_text() == self.integration.config_json("codex") | |
| 102 | + | |
| 103 | + # all editors covered | |
| 104 | + | |
| 105 | + def test_all_editors_supported(self): | |
| 106 | + for ed in SUPPORTED_EDITORS: | |
| 107 | + cfg = self.integration.config_for(ed) | |
| 108 | + assert "mcpServers" in cfg | |
| 109 | + | |
| 110 | + | |
| 111 | +# ── CLI tests ───────────────────────────────────────────────────────────────── | |
| 112 | + | |
| 113 | + | |
| 114 | +class TestEditorSetupCommand: | |
| 115 | + # Basic output | |
| 116 | + | |
| 117 | + def test_claude_code_outputs_json(self): | |
| 118 | + runner = CliRunner() | |
| 119 | + result = runner.invoke(main, ["editor", "setup", "claude-code"]) | |
| 120 | + assert result.exit_code == 0 | |
| 121 | + parsed = json.loads(result.output) | |
| 122 | + assert "mcpServers" in parsed | |
| 123 | + assert parsed["mcpServers"]["navegador"]["command"] == "navegador" | |
| 124 | + | |
| 125 | + def test_cursor_outputs_json(self): | |
| 126 | + runner = CliRunner() | |
| 127 | + result = runner.invoke(main, ["editor", "setup", "cursor"]) | |
| 128 | + assert result.exit_code == 0 | |
| 129 | + parsed = json.loads(result.output) | |
| 130 | + assert "mcpServers" in parsed | |
| 131 | + | |
| 132 | + def test_codex_outputs_json(self): | |
| 133 | + runner = CliRunner() | |
| 134 | + result = runner.invoke(main, ["editor", "setup", "codex"]) | |
| 135 | + assert result.exit_code == 0 | |
| 136 | + parsed = json.loads(result.output) | |
| 137 | + assert "mcpServers" in parsed | |
| 138 | + | |
| 139 | + def test_windsurf_outputs_json(self): | |
| 140 | + runner = CliRunner() | |
| 141 | + result = runner.invoke(main, ["editor", "setup", "windsurf"]) | |
| 142 | + assert result.exit_code == 0 | |
| 143 | + parsed = json.loads(result.output) | |
| 144 | + assert "mcpServers" in parsed | |
| 145 | + | |
| 146 | + # --db option | |
| 147 | + | |
| 148 | + def test_custom_db_reflected_in_output(self): | |
| 149 | + runner = CliRunner() | |
| 150 | + result = runner.invoke(main, ["editor", "setup", "cursor", "--db", "/custom/graph.db"]) | |
| 151 | + assert result.exit_code == 0 | |
| 152 | + parsed = json.loads(result.output) | |
| 153 | + assert parsed["mcpServers"]["navegador"]["args"][2] == "/custom/graph.db" | |
| 154 | + | |
| 155 | + # 'all' generates for all editors | |
| 156 | + | |
| 157 | + def test_all_generates_for_all_editors(self): | |
| 158 | + runner = CliRunner() | |
| 159 | + result = runner.invoke(main, ["editor", "setup", "all"]) | |
| 160 | + assert result.exit_code == 0 | |
| 161 | + # Each editor name should appear in the output header | |
| 162 | + for ed in SUPPORTED_EDITORS: | |
| 163 | + assert ed in result.output | |
| 164 | + | |
| 165 | + def test_all_output_contains_multiple_json_blocks(self): | |
| 166 | + runner = CliRunner() | |
| 167 | + result = runner.invoke(main, ["editor", "setup", "all"]) | |
| 168 | + assert result.exit_code == 0 | |
| 169 | + # Count occurrences of "mcpServers" — one per editor | |
| 170 | + assert result.output.count("mcpServers") == len(SUPPORTED_EDITORS) | |
| 171 | + | |
| 172 | + # Invalid editor name | |
| 173 | + | |
| 174 | + def test_invalid_editor_exits_nonzero(self): | |
| 175 | + runner = CliRunner() | |
| 176 | + result = runner.invoke(main, ["editor", "setup", "vscode"]) | |
| 177 | + assert result.exit_code != 0 | |
| 178 | + | |
| 179 | + def test_invalid_editor_shows_error(self): | |
| 180 | + runner = CliRunner() | |
| 181 | + result = runner.invoke(main, ["editor", "setup", "vim"]) | |
| 182 | + assert "vim" in result.output or "vim" in (result.exception or "") | |
| 183 | + | |
| 184 | + # --write flag | |
| 185 | + | |
| 186 | + def test_write_flag_creates_file(self): | |
| 187 | + runner = CliRunner() | |
| 188 | + with runner.isolated_filesystem(): | |
| 189 | + result = runner.invoke(main, ["editor", "setup", "claude-code", "--write"]) | |
| 190 | + assert result.exit_code == 0 | |
| 191 | + written = Path(".claude/mcp.json") | |
| 192 | + assert written.exists() | |
| 193 | + parsed = json.loads(written.read_text()) | |
| 194 | + assert "mcpServers" in parsed | |
| 195 | + | |
| 196 | + def test_write_flag_cursor_creates_correct_file(self): | |
| 197 | + runner = CliRunner() | |
| 198 | + with runner.isolated_filesystem(): | |
| 199 | + result = runner.invoke(main, ["editor", "setup", "cursor", "--write"]) | |
| 200 | + assert result.exit_code == 0 | |
| 201 | + assert Path(".cursor/mcp.json").exists() | |
| 202 | + | |
| 203 | + def test_write_flag_all_creates_all_files(self): | |
| 204 | + runner = CliRunner() | |
| 205 | + with runner.isolated_filesystem(): | |
| 206 | + result = runner.invoke(main, ["editor", "setup", "all", "--write"]) | |
| 207 | + assert result.exit_code == 0 | |
| 208 | + assert Path(".claude/mcp.json").exists() | |
| 209 | + assert Path(".cursor/mcp.json").exists() | |
| 210 | + assert Path(".codex/config.json").exists() | |
| 211 | + assert Path(".windsurf/mcp.json").exists() | |
| 212 | + | |
| 213 | + def test_write_flag_shows_written_path(self): | |
| 214 | + runner = CliRunner() | |
| 215 | + with runner.isolated_filesystem(): | |
| 216 | + result = runner.invoke(main, ["editor", "setup", "windsurf", "--write"]) | |
| 217 | + assert result.exit_code == 0 | |
| 218 | + assert "Written" in result.output or ".windsurf" in result.output |
| --- a/tests/test_editor.py | |
| +++ b/tests/test_editor.py | |
| @@ -0,0 +1,218 @@ | |
| --- a/tests/test_editor.py | |
| +++ b/tests/test_editor.py | |
| @@ -0,0 +1,218 @@ | |
| 1 | """Tests for editor integration — EditorIntegration class and CLI command.""" |
| 2 | |
| 3 | import json |
| 4 | from pathlib import Path |
| 5 | from unittest.mock import MagicMock, patch |
| 6 | |
| 7 | import pytest |
| 8 | from click.testing import CliRunner |
| 9 | |
| 10 | from navegador.cli.commands import main |
| 11 | from navegador.editor import SUPPORTED_EDITORS, EditorIntegration |
| 12 | |
| 13 | # ── EditorIntegration unit tests ────────────────────────────────────────────── |
| 14 | |
| 15 | |
| 16 | class TestEditorIntegration: |
| 17 | def setup_method(self): |
| 18 | self.integration = EditorIntegration(db=".navegador/graph.db") |
| 19 | |
| 20 | # config_for |
| 21 | |
| 22 | def test_config_for_claude_code(self): |
| 23 | cfg = self.integration.config_for("claude-code") |
| 24 | assert cfg["mcpServers"]["navegador"]["command"] == "navegador" |
| 25 | assert cfg["mcpServers"]["navegador"]["args"] == ["mcp", "--db", ".navegador/graph.db"] |
| 26 | |
| 27 | def test_config_for_cursor(self): |
| 28 | cfg = self.integration.config_for("cursor") |
| 29 | assert cfg["mcpServers"]["navegador"]["command"] == "navegador" |
| 30 | assert cfg["mcpServers"]["navegador"]["args"] == ["mcp", "--db", ".navegador/graph.db"] |
| 31 | |
| 32 | def test_config_for_codex(self): |
| 33 | cfg = self.integration.config_for("codex") |
| 34 | assert cfg["mcpServers"]["navegador"]["command"] == "navegador" |
| 35 | assert cfg["mcpServers"]["navegador"]["args"] == ["mcp", "--db", ".navegador/graph.db"] |
| 36 | |
| 37 | def test_config_for_windsurf(self): |
| 38 | cfg = self.integration.config_for("windsurf") |
| 39 | assert cfg["mcpServers"]["navegador"]["command"] == "navegador" |
| 40 | assert cfg["mcpServers"]["navegador"]["args"] == ["mcp", "--db", ".navegador/graph.db"] |
| 41 | |
| 42 | def test_config_for_invalid_editor_raises(self): |
| 43 | with pytest.raises(ValueError, match="Unsupported editor"): |
| 44 | self.integration.config_for("vscode") |
| 45 | |
| 46 | # custom db path |
| 47 | |
| 48 | def test_custom_db_path_reflected_in_config(self): |
| 49 | integration = EditorIntegration(db="/custom/path/graph.db") |
| 50 | cfg = integration.config_for("cursor") |
| 51 | assert cfg["mcpServers"]["navegador"]["args"][2] == "/custom/path/graph.db" |
| 52 | |
| 53 | # config_json |
| 54 | |
| 55 | def test_config_json_is_valid_json(self): |
| 56 | raw = self.integration.config_json("claude-code") |
| 57 | parsed = json.loads(raw) |
| 58 | assert "mcpServers" in parsed |
| 59 | |
| 60 | def test_config_json_is_pretty_printed(self): |
| 61 | raw = self.integration.config_json("cursor") |
| 62 | assert "\n" in raw # indented |
| 63 | |
| 64 | # config_path |
| 65 | |
| 66 | def test_config_path_claude_code(self): |
| 67 | assert self.integration.config_path("claude-code") == ".claude/mcp.json" |
| 68 | |
| 69 | def test_config_path_cursor(self): |
| 70 | assert self.integration.config_path("cursor") == ".cursor/mcp.json" |
| 71 | |
| 72 | def test_config_path_codex(self): |
| 73 | assert self.integration.config_path("codex") == ".codex/config.json" |
| 74 | |
| 75 | def test_config_path_windsurf(self): |
| 76 | assert self.integration.config_path("windsurf") == ".windsurf/mcp.json" |
| 77 | |
| 78 | def test_config_path_invalid_editor_raises(self): |
| 79 | with pytest.raises(ValueError, match="Unsupported editor"): |
| 80 | self.integration.config_path("sublime") |
| 81 | |
| 82 | # write_config |
| 83 | |
| 84 | def test_write_config_creates_file(self, tmp_path): |
| 85 | written = self.integration.write_config("claude-code", base_dir=str(tmp_path)) |
| 86 | assert written.exists() |
| 87 | parsed = json.loads(written.read_text()) |
| 88 | assert "mcpServers" in parsed |
| 89 | |
| 90 | def test_write_config_creates_parent_dirs(self, tmp_path): |
| 91 | written = self.integration.write_config("windsurf", base_dir=str(tmp_path)) |
| 92 | assert (tmp_path / ".windsurf").is_dir() |
| 93 | assert written.name == "mcp.json" |
| 94 | |
| 95 | def test_write_config_returns_path_object(self, tmp_path): |
| 96 | result = self.integration.write_config("cursor", base_dir=str(tmp_path)) |
| 97 | assert isinstance(result, Path) |
| 98 | |
| 99 | def test_write_config_content_matches_config_json(self, tmp_path): |
| 100 | written = self.integration.write_config("codex", base_dir=str(tmp_path)) |
| 101 | assert written.read_text() == self.integration.config_json("codex") |
| 102 | |
| 103 | # all editors covered |
| 104 | |
| 105 | def test_all_editors_supported(self): |
| 106 | for ed in SUPPORTED_EDITORS: |
| 107 | cfg = self.integration.config_for(ed) |
| 108 | assert "mcpServers" in cfg |
| 109 | |
| 110 | |
| 111 | # ── CLI tests ───────────────────────────────────────────────────────────────── |
| 112 | |
| 113 | |
| 114 | class TestEditorSetupCommand: |
| 115 | # Basic output |
| 116 | |
| 117 | def test_claude_code_outputs_json(self): |
| 118 | runner = CliRunner() |
| 119 | result = runner.invoke(main, ["editor", "setup", "claude-code"]) |
| 120 | assert result.exit_code == 0 |
| 121 | parsed = json.loads(result.output) |
| 122 | assert "mcpServers" in parsed |
| 123 | assert parsed["mcpServers"]["navegador"]["command"] == "navegador" |
| 124 | |
| 125 | def test_cursor_outputs_json(self): |
| 126 | runner = CliRunner() |
| 127 | result = runner.invoke(main, ["editor", "setup", "cursor"]) |
| 128 | assert result.exit_code == 0 |
| 129 | parsed = json.loads(result.output) |
| 130 | assert "mcpServers" in parsed |
| 131 | |
| 132 | def test_codex_outputs_json(self): |
| 133 | runner = CliRunner() |
| 134 | result = runner.invoke(main, ["editor", "setup", "codex"]) |
| 135 | assert result.exit_code == 0 |
| 136 | parsed = json.loads(result.output) |
| 137 | assert "mcpServers" in parsed |
| 138 | |
| 139 | def test_windsurf_outputs_json(self): |
| 140 | runner = CliRunner() |
| 141 | result = runner.invoke(main, ["editor", "setup", "windsurf"]) |
| 142 | assert result.exit_code == 0 |
| 143 | parsed = json.loads(result.output) |
| 144 | assert "mcpServers" in parsed |
| 145 | |
| 146 | # --db option |
| 147 | |
| 148 | def test_custom_db_reflected_in_output(self): |
| 149 | runner = CliRunner() |
| 150 | result = runner.invoke(main, ["editor", "setup", "cursor", "--db", "/custom/graph.db"]) |
| 151 | assert result.exit_code == 0 |
| 152 | parsed = json.loads(result.output) |
| 153 | assert parsed["mcpServers"]["navegador"]["args"][2] == "/custom/graph.db" |
| 154 | |
| 155 | # 'all' generates for all editors |
| 156 | |
| 157 | def test_all_generates_for_all_editors(self): |
| 158 | runner = CliRunner() |
| 159 | result = runner.invoke(main, ["editor", "setup", "all"]) |
| 160 | assert result.exit_code == 0 |
| 161 | # Each editor name should appear in the output header |
| 162 | for ed in SUPPORTED_EDITORS: |
| 163 | assert ed in result.output |
| 164 | |
| 165 | def test_all_output_contains_multiple_json_blocks(self): |
| 166 | runner = CliRunner() |
| 167 | result = runner.invoke(main, ["editor", "setup", "all"]) |
| 168 | assert result.exit_code == 0 |
| 169 | # Count occurrences of "mcpServers" — one per editor |
| 170 | assert result.output.count("mcpServers") == len(SUPPORTED_EDITORS) |
| 171 | |
| 172 | # Invalid editor name |
| 173 | |
| 174 | def test_invalid_editor_exits_nonzero(self): |
| 175 | runner = CliRunner() |
| 176 | result = runner.invoke(main, ["editor", "setup", "vscode"]) |
| 177 | assert result.exit_code != 0 |
| 178 | |
| 179 | def test_invalid_editor_shows_error(self): |
| 180 | runner = CliRunner() |
| 181 | result = runner.invoke(main, ["editor", "setup", "vim"]) |
| 182 | assert "vim" in result.output or "vim" in (result.exception or "") |
| 183 | |
| 184 | # --write flag |
| 185 | |
| 186 | def test_write_flag_creates_file(self): |
| 187 | runner = CliRunner() |
| 188 | with runner.isolated_filesystem(): |
| 189 | result = runner.invoke(main, ["editor", "setup", "claude-code", "--write"]) |
| 190 | assert result.exit_code == 0 |
| 191 | written = Path(".claude/mcp.json") |
| 192 | assert written.exists() |
| 193 | parsed = json.loads(written.read_text()) |
| 194 | assert "mcpServers" in parsed |
| 195 | |
| 196 | def test_write_flag_cursor_creates_correct_file(self): |
| 197 | runner = CliRunner() |
| 198 | with runner.isolated_filesystem(): |
| 199 | result = runner.invoke(main, ["editor", "setup", "cursor", "--write"]) |
| 200 | assert result.exit_code == 0 |
| 201 | assert Path(".cursor/mcp.json").exists() |
| 202 | |
| 203 | def test_write_flag_all_creates_all_files(self): |
| 204 | runner = CliRunner() |
| 205 | with runner.isolated_filesystem(): |
| 206 | result = runner.invoke(main, ["editor", "setup", "all", "--write"]) |
| 207 | assert result.exit_code == 0 |
| 208 | assert Path(".claude/mcp.json").exists() |
| 209 | assert Path(".cursor/mcp.json").exists() |
| 210 | assert Path(".codex/config.json").exists() |
| 211 | assert Path(".windsurf/mcp.json").exists() |
| 212 | |
| 213 | def test_write_flag_shows_written_path(self): |
| 214 | runner = CliRunner() |
| 215 | with runner.isolated_filesystem(): |
| 216 | result = runner.invoke(main, ["editor", "setup", "windsurf", "--write"]) |
| 217 | assert result.exit_code == 0 |
| 218 | assert "Written" in result.output or ".windsurf" in result.output |