test: add pytest suite for core modules
- Add pytest dependency and config to pyproject.toml - Test project_root: env var priority, fallback, directory creation - Test CLI: help, version, subcommands, default behavior, options
This commit is contained in:
@@ -11,6 +11,13 @@ dependencies = [
|
|||||||
[project.scripts]
|
[project.scripts]
|
||||||
myclaude = "myclaude.cli:cli"
|
myclaude = "myclaude.cli:cli"
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = ["pytest>=8.0"]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
python_files = ["test_*.py"]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["hatchling"]
|
requires = ["hatchling"]
|
||||||
build-backend = "hatchling.build"
|
build-backend = "hatchling.build"
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
"""Tests for myclaude.cli."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
from click.testing import CliRunner
|
||||||
|
|
||||||
|
from myclaude.cli import cli
|
||||||
|
|
||||||
|
|
||||||
|
class TestCliHelp:
|
||||||
|
"""Tests for CLI help and basic invocation."""
|
||||||
|
|
||||||
|
def test_help_shows_options(self) -> None:
|
||||||
|
"""--help should show cwd and browser control options."""
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(cli, ["--help"])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "--cwd" in result.output
|
||||||
|
assert "--dangerously-allow-browser-control" in result.output
|
||||||
|
|
||||||
|
def test_version_shows_version(self) -> None:
|
||||||
|
"""--version should show package version."""
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(cli, ["--version"])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "version" in result.output.lower()
|
||||||
|
|
||||||
|
def test_update_subcommand_exists(self) -> None:
|
||||||
|
"""update subcommand should be registered."""
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(cli, ["update", "--help"])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "update" in result.output.lower() or "reinstall" in result.output.lower()
|
||||||
|
|
||||||
|
def test_upgrade_alias_exists(self) -> None:
|
||||||
|
"""upgrade should be an alias for update."""
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(cli, ["upgrade", "--help"])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
|
||||||
|
class TestCliDefaultBehavior:
|
||||||
|
"""Tests for default chat behavior."""
|
||||||
|
|
||||||
|
def test_no_subcommand_runs_claude(self) -> None:
|
||||||
|
"""Running without subcommand should invoke claude binary."""
|
||||||
|
runner = CliRunner()
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("myclaude.cli.shutil.which", return_value="/usr/bin/claude"),
|
||||||
|
patch("myclaude.cli.subprocess.run") as mock_run,
|
||||||
|
):
|
||||||
|
mock_run.return_value = MagicMock(returncode=0)
|
||||||
|
result = runner.invoke(cli, [])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
mock_run.assert_called_once()
|
||||||
|
call_args = mock_run.call_args
|
||||||
|
assert call_args[0][0] == ["/usr/bin/claude"]
|
||||||
|
|
||||||
|
def test_missing_claude_binary_error(self) -> None:
|
||||||
|
"""Should exit with error when claude binary not found."""
|
||||||
|
runner = CliRunner()
|
||||||
|
|
||||||
|
with patch("myclaude.cli.shutil.which", return_value=None):
|
||||||
|
result = runner.invoke(cli, [])
|
||||||
|
assert result.exit_code == 127
|
||||||
|
assert "not found" in result.output.lower()
|
||||||
|
|
||||||
|
def test_cwd_option_passed(self, tmp_path: Path) -> None:
|
||||||
|
"""--cwd should be passed as working directory."""
|
||||||
|
runner = CliRunner()
|
||||||
|
test_dir = tmp_path / "test_cwd"
|
||||||
|
test_dir.mkdir()
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("myclaude.cli.shutil.which", return_value="/usr/bin/claude"),
|
||||||
|
patch("myclaude.cli.subprocess.run") as mock_run,
|
||||||
|
):
|
||||||
|
mock_run.return_value = MagicMock(returncode=0)
|
||||||
|
result = runner.invoke(cli, ["--cwd", str(test_dir)])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
call_kwargs = mock_run.call_args.kwargs
|
||||||
|
assert call_kwargs.get("cwd") == str(test_dir.resolve())
|
||||||
|
|
||||||
|
def test_dangerously_allow_browser_control_flag(self) -> None:
|
||||||
|
"""--dangerously-allow-browser-control should be forwarded."""
|
||||||
|
runner = CliRunner()
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("myclaude.cli.shutil.which", return_value="/usr/bin/claude"),
|
||||||
|
patch("myclaude.cli.subprocess.run") as mock_run,
|
||||||
|
):
|
||||||
|
mock_run.return_value = MagicMock(returncode=0)
|
||||||
|
result = runner.invoke(cli, ["--dangerously-allow-browser-control"])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
call_args = mock_run.call_args[0][0]
|
||||||
|
assert "--dangerously-allow-browser-control" in call_args
|
||||||
|
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
"""Tests for myclaude.project_root."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from myclaude.project_root import get_myclaude_project_root, get_workspace_root
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetMyclaudeProjectRoot:
|
||||||
|
"""Tests for get_myclaude_project_root."""
|
||||||
|
|
||||||
|
def test_env_var_takes_priority(self, tmp_path: Path) -> None:
|
||||||
|
"""MYCLAUDE_PROJECT_ROOT env var should be used when set."""
|
||||||
|
fake_root = tmp_path / "fake_repo"
|
||||||
|
fake_root.mkdir()
|
||||||
|
(fake_root / "pyproject.toml").write_text('name = "myclaude"\n')
|
||||||
|
|
||||||
|
with patch.dict(os.environ, {"MYCLAUDE_PROJECT_ROOT": str(fake_root)}):
|
||||||
|
result = get_myclaude_project_root()
|
||||||
|
assert result == fake_root.resolve()
|
||||||
|
|
||||||
|
def test_fallback_when_not_in_repo(self) -> None:
|
||||||
|
"""When not in a repo, fallback to ~/.myclaude."""
|
||||||
|
with patch.dict(os.environ, {}, clear=True):
|
||||||
|
with patch("pathlib.Path.cwd", side_effect=OSError):
|
||||||
|
with patch("myclaude.project_root._pyproject_names_myclaude", return_value=False):
|
||||||
|
result = get_myclaude_project_root()
|
||||||
|
assert result == Path.home() / ".myclaude"
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetWorkspaceRoot:
|
||||||
|
"""Tests for get_workspace_root."""
|
||||||
|
|
||||||
|
def test_env_var_takes_priority(self, tmp_path: Path) -> None:
|
||||||
|
"""MYCLAUDE_WORKSPACE_ROOT env var should be used when set."""
|
||||||
|
custom = tmp_path / "custom_workspace"
|
||||||
|
|
||||||
|
with patch.dict(os.environ, {"MYCLAUDE_WORKSPACE_ROOT": str(custom)}):
|
||||||
|
result = get_workspace_root()
|
||||||
|
assert result == custom.resolve()
|
||||||
|
assert result.is_dir()
|
||||||
|
|
||||||
|
def test_creates_directory_if_missing(self, tmp_path: Path) -> None:
|
||||||
|
"""Should create the workspace directory if it doesn't exist."""
|
||||||
|
new_ws = tmp_path / "new_workspace"
|
||||||
|
assert not new_ws.exists()
|
||||||
|
|
||||||
|
with patch.dict(os.environ, {"MYCLAUDE_WORKSPACE_ROOT": str(new_ws)}):
|
||||||
|
result = get_workspace_root()
|
||||||
|
assert result == new_ws.resolve()
|
||||||
|
assert result.is_dir()
|
||||||
|
|
||||||
|
def test_defaults_to_project_workspace(self, tmp_path: Path) -> None:
|
||||||
|
"""When project_root/workspace exists, use it."""
|
||||||
|
project_root = tmp_path / "myclaude"
|
||||||
|
project_root.mkdir()
|
||||||
|
workspace = project_root / "workspace"
|
||||||
|
workspace.mkdir()
|
||||||
|
(project_root / "pyproject.toml").write_text('name = "myclaude"\n')
|
||||||
|
|
||||||
|
with patch.dict(os.environ, {"MYCLAUDE_PROJECT_ROOT": str(project_root)}, clear=True):
|
||||||
|
result = get_workspace_root()
|
||||||
|
assert result == workspace.resolve()
|
||||||
|
|
||||||
|
def test_defaults_to_home_workspace(self, tmp_path: Path) -> None:
|
||||||
|
"""When no env var and no project workspace, default to ~/workspace."""
|
||||||
|
with (
|
||||||
|
patch.dict(os.environ, {}, clear=True),
|
||||||
|
patch("myclaude.project_root.get_myclaude_project_root", return_value=tmp_path / ".myclaude"),
|
||||||
|
patch.dict(os.environ, {}, clear=True),
|
||||||
|
):
|
||||||
|
with patch.dict(os.environ, {}, clear=True):
|
||||||
|
# Ensure HOME is set for Path.home()
|
||||||
|
home = tmp_path / "home"
|
||||||
|
home.mkdir()
|
||||||
|
with patch("pathlib.Path.home", return_value=home):
|
||||||
|
result = get_workspace_root()
|
||||||
|
assert result == home / "workspace"
|
||||||
|
assert result.is_dir()
|
||||||
Reference in New Issue
Block a user