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
15 changes: 0 additions & 15 deletions .npmignore

This file was deleted.

8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ cd trushell
pip install -e ".[dev]"
```

After installation, the `trushell` console script is added to your shell `PATH` and can be run directly.

If you are working from the source tree without installing, run:

```bash
PYTHONPATH=. python -m trushell
```

## Quick Start

```bash
Expand Down
2 changes: 2 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

157 changes: 148 additions & 9 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@
_handle_edit_command,
_handle_local_command,
_handle_os_fallback,
_prompt_command,
_run_external_command,
_split_command,
is_dangerous_command,
parse_and_execute_command,
)


Expand All @@ -33,7 +37,7 @@ def test_help_shows_usage() -> None:
def test_unknown_command_uses_os_fallback(monkeypatch) -> None:
calls = {}

def fake_run(command: str, shell: bool, check: bool, cwd: str) -> subprocess.CompletedProcess[str]:
def fake_run(command: list[str], shell: bool, check: bool, cwd: str) -> subprocess.CompletedProcess[str]:
calls["command"] = command
calls["shell"] = shell
calls["check"] = check
Expand All @@ -43,7 +47,131 @@ def fake_run(command: str, shell: bool, check: bool, cwd: str) -> subprocess.Com
monkeypatch.setattr("trushell.project._run_external_command", fake_run)

assert _handle_os_fallback("pwd") is True
assert calls == {"command": "pwd", "shell": True, "check": False, "cwd": os.getcwd()}
assert calls == {
"command": ["pwd"],
"shell": False,
"check": False,
"cwd": os.getcwd(),
}


def test_is_dangerous_command_blocks_shell_meta() -> None:
assert is_dangerous_command("ls | grep foo") is True
assert is_dangerous_command("echo $HOME") is True
assert is_dangerous_command("rm file; echo hi") is True
assert is_dangerous_command("echo hello") is False
assert is_dangerous_command('echo "hello world"') is False


def test_handle_local_command_prioritizes_todo(monkeypatch) -> None:
calls = {}

def fake_addtask(task: str, category: str) -> None:
calls["task"] = task
calls["category"] = category

monkeypatch.setattr("trushell.project.addtask", fake_addtask)

command, arguments = _split_command('addtask "Review PR" "Work"')
result = _handle_local_command(command, arguments)

assert result == "handled"
assert calls == {"task": "Review PR", "category": "Work"}


def test_handle_local_command_addtask_escaped_quotes(monkeypatch) -> None:
calls = {}

def fake_addtask(task: str, category: str) -> None:
calls["task"] = task
calls["category"] = category

monkeypatch.setattr("trushell.project.addtask", fake_addtask)

command, arguments = _split_command(
'addtask "Finish report with \\"quotes\\" inside" "Work"'
)
result = _handle_local_command(command, arguments)

assert result == "handled"
assert calls == {
"task": 'Finish report with "quotes" inside',
"category": "Work",
}


def test_split_command_returns_empty_on_unclosed_quotes() -> None:
command, arguments = _split_command('addtask "Unclosed quote')

assert command == ""
assert arguments == []


def test_prompt_command_warns_on_multiline_input(monkeypatch) -> None:
messages = []

monkeypatch.setattr("builtins.input", lambda prompt: "echo hi\nls")
monkeypatch.setattr(
"trushell.project.typer.secho",
lambda message, fg=None: messages.append((message, fg)),
)

raw, command, arguments = _prompt_command()

assert raw == ""
assert command == ""
assert arguments == []
assert messages and "Please enter commands one at a time" in messages[0][0]


def test_prompt_command_warns_on_invalid_syntax(monkeypatch) -> None:
messages = []

monkeypatch.setattr("builtins.input", lambda prompt: 'addtask "Unclosed quote')
monkeypatch.setattr(
"trushell.project.typer.secho",
lambda message, fg=None: messages.append((message, fg)),
)

raw, command, arguments = _prompt_command()

assert raw == ""
assert command == ""
assert arguments == []
assert messages and "Invalid syntax" in messages[0][0]


def test_parse_and_execute_command_handles_unclosed_quotes(monkeypatch) -> None:
messages = []

monkeypatch.setattr(
"trushell.project.typer.secho",
lambda message, fg=None: messages.append((message, fg)),
)

assert parse_and_execute_command('addtask "Unclosed quote') is True
assert messages and "Invalid syntax" in messages[0][0]


def test_parse_and_execute_command_runs_simple_external(monkeypatch) -> None:
calls = {}

def fake_run(command: list[str], shell: bool, check: bool, cwd: str) -> subprocess.CompletedProcess[str]:
calls["command"] = command
calls["shell"] = shell
calls["check"] = check
calls["cwd"] = cwd
return subprocess.CompletedProcess(args=command, returncode=0)

monkeypatch.setattr("trushell.project._run_external_command", fake_run)

assert parse_and_execute_command("echo hi") is True
assert calls == {
"command": ["echo", "hi"],
"shell": False,
"check": False,
"cwd": os.getcwd(),
}


def test_run_external_command_profiles_resources(monkeypatch) -> None:
Expand Down Expand Up @@ -98,7 +226,7 @@ def test_cd_command_changes_directory_and_runs_ls(monkeypatch) -> None:
def fake_chdir(path: str) -> None:
calls["chdir"] = path

def fake_run(command: str, shell: bool, check: bool, cwd: str) -> SimpleNamespace:
def fake_run(command: list[str], shell: bool, check: bool, cwd: str) -> SimpleNamespace:
calls["ls_command"] = command
calls["ls_shell"] = shell
calls["ls_check"] = check
Expand All @@ -108,8 +236,15 @@ def fake_run(command: str, shell: bool, check: bool, cwd: str) -> SimpleNamespac
monkeypatch.setattr("trushell.project.os.chdir", fake_chdir)
monkeypatch.setattr("trushell.project._run_external_command", fake_run)

assert _handle_cd_command("cd /tmp") is True
assert calls == {"chdir": "/tmp", "ls_command": "ls", "ls_shell": True, "ls_check": False, "ls_cwd": os.getcwd()}
command, arguments = _split_command("cd /tmp")
assert _handle_cd_command(command, arguments) is True
assert calls == {
"chdir": "/tmp",
"ls_command": ["ls"],
"ls_shell": False,
"ls_check": False,
"ls_cwd": os.getcwd(),
}


def test_cd_without_target_prints_syntax_hint(monkeypatch) -> None:
Expand All @@ -127,7 +262,8 @@ def fake_run(command: str, shell: bool, check: bool, cwd: str) -> SimpleNamespac
monkeypatch.setattr("trushell.project.os.chdir", fake_chdir)
monkeypatch.setattr("trushell.project.subprocess.run", fake_run)

assert _handle_cd_command("cd") is True
command, arguments = _split_command("cd")
assert _handle_cd_command(command, arguments) is True
assert calls == {}


Expand All @@ -139,7 +275,8 @@ def test_addtask_missing_arguments_is_blocked(monkeypatch) -> None:
lambda message, fg=None: messages.append((message, fg)),
)

assert _handle_local_command("addtask", "") == "handled"
command, arguments = _split_command("addtask")
assert _handle_local_command(command, arguments) == "handled"


def test_edit_requires_filename(monkeypatch) -> None:
Expand All @@ -150,7 +287,8 @@ def test_edit_requires_filename(monkeypatch) -> None:
lambda message, fg=None: messages.append((message, fg)),
)

assert _handle_edit_command("edit") is True
command, arguments = _split_command("edit")
assert _handle_edit_command(command, arguments) is True
assert messages and "Syntax: edit <filename>" in messages[0][0]


Expand All @@ -170,7 +308,8 @@ def run(self) -> None:

monkeypatch.setattr("trushell.project.TruShellEditor", FakeEditor)

assert _handle_edit_command(f"edit {file_path}") is True
command, arguments = _split_command(f"edit {file_path}")
assert _handle_edit_command(command, arguments) is True
assert calls == {"filename": str(file_path), "initial_text": "hello", "ran": True}


Expand Down
Loading
Loading