Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2025 cebidhem

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
20 changes: 0 additions & 20 deletions specs/ez-leaf.md

This file was deleted.

67 changes: 39 additions & 28 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,47 +32,58 @@ def test_version_option(self):
class TestCLICreate:
"""Tests for create command."""

def test_create_worktree_default_mode(self, mocker):
"""Test creating worktree with default mode."""
def test_create_worktree_default(self, mocker):
"""Test creating worktree without any flags."""
mock_create = mocker.patch(
"wt.cli.create_worktree", return_value=Path("/path/to/worktree")
)
mock_handle_mode = mocker.patch("wt.cli.handle_mode")
mock_print = mocker.patch("builtins.print")

result = runner.invoke(app, ["create", "feature-x"])

assert result.exit_code == 0
mock_create.assert_called_once_with("feature-x")
mock_handle_mode.assert_called_once_with(
"none", Path("/path/to/worktree"), None
)
mock_print.assert_called_once_with("Worktree created at: /path/to/worktree")

def test_create_worktree_terminal_mode(self, mocker):
"""Test creating worktree with terminal mode."""
def test_create_worktree_with_ide(self, mocker):
"""Test creating worktree with --ide flag."""
mocker.patch("wt.cli.create_worktree", return_value=Path("/path/to/worktree"))
mock_handle_mode = mocker.patch("wt.cli.handle_mode")
mock_launch_ide = mocker.patch("wt.cli.launch_ide")

result = runner.invoke(app, ["create", "feature-x", "--mode", "terminal"])
result = runner.invoke(app, ["create", "feature-x", "--ide", "code"])

assert result.exit_code == 0
mock_handle_mode.assert_called_once_with(
"terminal", Path("/path/to/worktree"), None
)
mock_launch_ide.assert_called_once_with(Path("/path/to/worktree"), "code")

def test_create_worktree_ide_mode(self, mocker):
"""Test creating worktree with IDE mode."""
def test_create_worktree_with_claude(self, mocker):
"""Test creating worktree with --claude flag."""
mocker.patch("wt.cli.create_worktree", return_value=Path("/path/to/worktree"))
mock_handle_mode = mocker.patch("wt.cli.handle_mode")
mock_launch_claude = mocker.patch("wt.cli.launch_claude")

result = runner.invoke(
app, ["create", "feature-x", "--mode", "ide", "--ide", "code"]
)
result = runner.invoke(app, ["create", "feature-x", "--claude"])

assert result.exit_code == 0
mock_handle_mode.assert_called_once_with(
"ide", Path("/path/to/worktree"), "code"
mock_launch_claude.assert_called_once_with(Path("/path/to/worktree"))

def test_create_worktree_ide_and_claude_exclusive(self, mocker):
"""Test that --ide and --claude are mutually exclusive."""
mock_echo = mocker.patch("typer.echo")
mocker.patch("wt.cli.create_worktree", return_value=Path("/path/to/worktree"))

result = runner.invoke(
app, ["create", "feature-x", "--ide", "code", "--claude"]
)

assert result.exit_code == 1
# Verify error message was echoed
mock_echo.assert_called()
error_call = [
call
for call in mock_echo.call_args_list
if call.args and "mutually exclusive" in call.args[0]
]
assert len(error_call) > 0

def test_create_worktree_error(self, mocker):
"""Test creating worktree when WorktreeError occurs."""
mock_echo = mocker.patch("typer.echo")
Expand All @@ -86,25 +97,25 @@ def test_create_worktree_error(self, mocker):
error_call = [
call
for call in mock_echo.call_args_list
if "Error: Test error" in str(call)
if call.args and "Error: Test error" in call.args[0]
]
assert len(error_call) > 0

def test_create_worktree_launcher_error(self, mocker):
"""Test creating worktree when LauncherError occurs."""
mock_echo = mocker.patch("typer.echo")
mocker.patch("wt.cli.create_worktree", return_value=Path("/path/to/worktree"))
mocker.patch("wt.cli.handle_mode", side_effect=LauncherError("Launcher error"))
mocker.patch("wt.cli.launch_ide", side_effect=LauncherError("Launcher error"))

result = runner.invoke(app, ["create", "feature-x", "--mode", "ide"])
result = runner.invoke(app, ["create", "feature-x", "--ide", "code"])

assert result.exit_code == 1
# Verify error message was echoed
mock_echo.assert_called()
error_call = [
call
for call in mock_echo.call_args_list
if "Error: Launcher error" in str(call)
if call.args and "Error: Launcher error" in call.args[0]
]
assert len(error_call) > 0

Expand Down Expand Up @@ -157,7 +168,7 @@ def test_list_worktrees_error(self, mocker):
error_call = [
call
for call in mock_echo.call_args_list
if "Error: List error" in str(call)
if call.args and "Error: List error" in call.args[0]
]
assert len(error_call) > 0

Expand Down Expand Up @@ -199,7 +210,7 @@ def test_delete_worktree_error(self, mocker):
error_call = [
call
for call in mock_echo.call_args_list
if "Error: Delete error" in str(call)
if call.args and "Error: Delete error" in call.args[0]
]
assert len(error_call) > 0

Expand All @@ -224,8 +235,8 @@ def test_create_help(self):

assert result.exit_code == 0
assert "Create a new git worktree" in output
assert "--mode" in output
assert "--ide" in output
assert "--claude" in output

def test_list_help(self):
"""Test list command help."""
Expand Down
74 changes: 51 additions & 23 deletions tests/test_launchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
LauncherError,
launch_ide,
launch_terminal,
launch_claude,
_launch_iterm2,
_command_exists,
handle_mode,
)


Expand Down Expand Up @@ -103,6 +103,15 @@ def test_launch_terminal_macos(self, mocker, tmp_path):

mock_launch_iterm2.assert_called_once_with(tmp_path)

def test_launch_terminal_linux(self, mocker, tmp_path):
"""Test launching terminal on Linux."""
mocker.patch("platform.system", return_value="Linux")
mock_launch_linux = mocker.patch("wt.launchers._launch_linux_terminal")

launch_terminal(tmp_path)

mock_launch_linux.assert_called_once_with(tmp_path)

def test_launch_terminal_unsupported_platform(self, mocker, tmp_path):
"""Test launching terminal on unsupported platform."""
mocker.patch("platform.system", return_value="Windows")
Expand All @@ -127,6 +136,20 @@ def test_launch_iterm2_success(self, mocker, tmp_path):
assert str(tmp_path) in args[2]
mock_print.assert_called_once()

def test_launch_iterm2_with_command(self, mocker, tmp_path):
"""Test launching iTerm2 with a command."""
mock_run = mocker.patch("subprocess.run", return_value=Mock(returncode=0))
mock_print = mocker.patch("builtins.print")

_launch_iterm2(tmp_path, command="claude")

mock_run.assert_called_once()
args = mock_run.call_args[0][0]
assert args[0] == "osascript"
assert str(tmp_path) in args[2]
assert "claude" in args[2]
mock_print.assert_called_once()

def test_launch_iterm2_failure(self, mocker, tmp_path):
"""Test launching iTerm2 when command fails."""
mocker.patch(
Expand All @@ -138,35 +161,40 @@ def test_launch_iterm2_failure(self, mocker, tmp_path):
_launch_iterm2(tmp_path)


class TestHandleMode:
"""Tests for handle_mode function."""
class TestLaunchClaude:
"""Tests for launch_claude function."""

def test_handle_mode_none(self, mocker, tmp_path):
"""Test handle_mode with mode='none'."""
mock_print = mocker.patch("builtins.print")
def test_launch_claude_macos(self, mocker, tmp_path):
"""Test launching Claude on macOS."""
mocker.patch("wt.launchers._command_exists", return_value=True)
mocker.patch("platform.system", return_value="Darwin")
mock_launch_iterm2 = mocker.patch("wt.launchers._launch_iterm2")

handle_mode("none", tmp_path)
launch_claude(tmp_path)

mock_print.assert_called_once()
assert "Worktree created at" in mock_print.call_args[0][0]
mock_launch_iterm2.assert_called_once_with(tmp_path, command="claude")

def test_handle_mode_terminal(self, mocker, tmp_path):
"""Test handle_mode with mode='terminal'."""
mock_launch_terminal = mocker.patch("wt.launchers.launch_terminal")
def test_launch_claude_linux(self, mocker, tmp_path):
"""Test launching Claude on Linux."""
mocker.patch("wt.launchers._command_exists", return_value=True)
mocker.patch("platform.system", return_value="Linux")
mock_launch_linux = mocker.patch("wt.launchers._launch_linux_terminal")

handle_mode("terminal", tmp_path)
launch_claude(tmp_path)

mock_launch_terminal.assert_called_once_with(tmp_path)
mock_launch_linux.assert_called_once_with(tmp_path, command="claude")

def test_handle_mode_ide(self, mocker, tmp_path):
"""Test handle_mode with mode='ide'."""
mock_launch_ide = mocker.patch("wt.launchers.launch_ide")
def test_launch_claude_not_installed(self, mocker, tmp_path):
"""Test launching Claude when CLI is not installed."""
mocker.patch("wt.launchers._command_exists", return_value=False)

handle_mode("ide", tmp_path, "code")
with pytest.raises(LauncherError, match="Claude CLI not found"):
launch_claude(tmp_path)

mock_launch_ide.assert_called_once_with(tmp_path, "code")
def test_launch_claude_unsupported_platform(self, mocker, tmp_path):
"""Test launching Claude on unsupported platform."""
mocker.patch("wt.launchers._command_exists", return_value=True)
mocker.patch("platform.system", return_value="Windows")

def test_handle_mode_unknown(self, tmp_path):
"""Test handle_mode with unknown mode."""
with pytest.raises(LauncherError, match="Unknown mode"):
handle_mode("invalid", tmp_path)
with pytest.raises(LauncherError, match="not supported on Windows"):
launch_claude(tmp_path)
3 changes: 3 additions & 0 deletions tests/test_worktree.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ def test_create_worktree_new_branch(self, mocker):
"wt.worktree.branch_exists", return_value=False
)
mock_run = mocker.patch("subprocess.run", return_value=Mock(returncode=0))
mock_copy_claude = mocker.patch("wt.worktree.copy_claude_settings")

result = create_worktree("feature-x")

Expand All @@ -132,6 +133,7 @@ def test_create_worktree_new_branch(self, mocker):
assert args[:3] == ["git", "worktree", "add"]
assert "-b" in args
assert "feature-x" in args
mock_copy_claude.assert_called_once()

def test_create_worktree_existing_branch(self, mocker):
"""Test creating worktree with an existing branch."""
Expand All @@ -142,6 +144,7 @@ def test_create_worktree_existing_branch(self, mocker):
)
mocker.patch("wt.worktree.branch_exists", return_value=True)
mock_run = mocker.patch("subprocess.run", return_value=Mock(returncode=0))
mocker.patch("wt.worktree.copy_claude_settings")

result = create_worktree("main")

Expand Down
Loading