Skip to content

Commit 344986a

Browse files
author
Ziang Zhang
committed
merge: resolve cli.py conflict with upstream/main
2 parents 618ddd5 + 7ab2e53 commit 344986a

10 files changed

Lines changed: 200 additions & 35 deletions

File tree

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -171,9 +171,9 @@ Settings are saved automatically.
171171

172172
<a href="https://www.star-history.com/?repos=AkshajSinghal%2FTruShell&type=date&legend=top-left">
173173
<picture>
174-
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=AkshajSinghal/TruShell&type=date&theme=dark&legend=top-left" />
175-
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=AkshajSinghal/TruShell&type=date&legend=top-left" />
176-
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=AkshajSinghal/TruShell&type=date&legend=top-left" />
174+
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=TruFoundation/TruShell&type=date&theme=dark&legend=top-left" />
175+
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=TruFoundation/TruShell&type=date&legend=top-left" />
176+
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=TruFoundation/TruShell&type=date&legend=top-left" />
177177
</picture>
178178
</a>
179179

pyproject.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ dependencies = [
3535
]
3636

3737
[project.urls]
38-
Home = "https://github.com/AkshajSinghal/TruShell"
39-
Repository = "https://github.com/AkshajSinghal/TruShell"
40-
Documentation = "https://github.com/AkshajSinghal/TruShell#readme"
41-
"Bug Tracker" = "https://github.com/AkshajSinghal/TruShell/issues"
38+
Home = "https://github.com/TruFoundation/TruShell"
39+
Repository = "https://github.com/TruFoundation/TruShell"
40+
Documentation = "https://github.com/TruFoundation/TruShell#readme"
41+
"Bug Tracker" = "https://github.com/TruFoundation/TruShell/issues"
4242

4343
[project.optional-dependencies]
4444
dev = ["pytest>=8.0.0", "ruff>=0.0.280", "black>=24.10.0"]

tests/conftest.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,34 @@
11
"""Pytest configuration and shared fixtures for TruShell tests."""
22

3+
import sqlite3
4+
35
import pytest
6+
7+
from trushell.core import database
48
from trushell.core.plugin_manager import PluginManager
59

610

11+
@pytest.fixture
12+
def in_memory_database(monkeypatch):
13+
"""Use one in-memory SQLite connection for database CRUD tests."""
14+
connection = sqlite3.connect(":memory:", check_same_thread=False)
15+
connection.execute(
16+
"""CREATE TABLE todos (
17+
task TEXT,
18+
category TEXT,
19+
date_added TEXT,
20+
date_completed TEXT,
21+
status INTEGER,
22+
position INTEGER
23+
)"""
24+
)
25+
monkeypatch.setattr(database, "get_db_connection", lambda: connection)
26+
27+
yield connection
28+
29+
connection.close()
30+
31+
732
@pytest.fixture(autouse=True)
833
def reset_plugin_manager():
934
"""Reset PluginManager singleton after each test to ensure isolation."""

tests/test_cli_os_fallback.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from __future__ import annotations
2+
3+
import os
4+
import subprocess
5+
6+
from trushell import cli
7+
8+
9+
def test_os_fallback_runs_argument_vector_without_shell(monkeypatch):
10+
calls = {}
11+
12+
def fake_run(command, shell, check, cwd):
13+
calls.update(
14+
command=command,
15+
shell=shell,
16+
check=check,
17+
cwd=cwd,
18+
)
19+
return subprocess.CompletedProcess(args=command, returncode=0)
20+
21+
monkeypatch.setattr(cli, "_run_external_command", fake_run)
22+
23+
assert cli._handle_os_fallback('echo "hello world"') is True
24+
assert calls == {
25+
"command": ["echo", "hello world"],
26+
"shell": False,
27+
"check": False,
28+
"cwd": os.getcwd(),
29+
}
30+
31+
32+
def test_os_fallback_does_not_interpret_shell_metacharacters(monkeypatch):
33+
calls = {}
34+
35+
def fake_run(command, shell, check, cwd):
36+
calls.update(command=command, shell=shell)
37+
return subprocess.CompletedProcess(args=command, returncode=0)
38+
39+
monkeypatch.setattr(cli, "_run_external_command", fake_run)
40+
41+
assert cli._handle_os_fallback("echo safe; touch injected") is True
42+
assert calls == {
43+
"command": ["echo", "safe;", "touch", "injected"],
44+
"shell": False,
45+
}
46+
47+
48+
def test_os_fallback_handles_malformed_quoting(monkeypatch, capsys):
49+
def fail_if_called(*args, **kwargs):
50+
raise AssertionError("external command should not run")
51+
52+
monkeypatch.setattr(cli, "_run_external_command", fail_if_called)
53+
54+
assert cli._handle_os_fallback('echo "unterminated') is True
55+
assert "OS fallback error" in capsys.readouterr().out

tests/test_data.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,26 @@ def test_run_csv_view_shows_limited_rows(tmp_path: Path) -> None:
3939
assert "User 50" in output
4040
assert "User 51" not in output
4141
assert "...and 3 more rows" in output
42+
43+
44+
def test_run_csv_view_short_rows_are_padded(tmp_path: Path) -> None:
45+
"""Ensure ragged rows are padded with empty cells.
46+
47+
Use quoted fields so csv.Sniffer has sufficient signal to detect
48+
the delimiter reliably on small samples.
49+
"""
50+
from trushell.commands.data import run_csv_view
51+
52+
file_path = tmp_path / "short_rows.csv"
53+
rows = ['"A","B","C"', '"1","2"', '"3","4","5"']
54+
file_path.write_text("\n".join(rows), encoding="utf-8")
55+
56+
output = _strip_ansi(run_csv_view(str(file_path)))
57+
58+
assert "A" in output
59+
assert "B" in output
60+
assert "C" in output
61+
assert "1" in output
62+
assert "3" in output
63+
# There should be at least one empty/padded cell visible in output
64+
assert " " in output

tests/test_database.py

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
from trushell.core.database import _ensure_initialized, get_all_todos, get_db_connection, insert_todo
1+
from trushell.core.database import (
2+
_ensure_initialized,
3+
get_all_todos,
4+
get_db_connection,
5+
insert_todo,
6+
)
27
from trushell.core.models import Todo
38

49

@@ -20,10 +25,7 @@ def test_get_db_connection_returns_fresh_connection(monkeypatch, tmp_path) -> No
2025
conn_two.close()
2126

2227

23-
def test_insert_todo_assigns_sequential_positions(monkeypatch, tmp_path) -> None:
24-
_use_temp_database(monkeypatch, tmp_path)
25-
26-
_ensure_initialized()
28+
def test_insert_todo_assigns_sequential_positions(in_memory_database) -> None:
2729
insert_todo(Todo(task="first", category="work"))
2830
insert_todo(Todo(task="second", category="work"))
2931

@@ -33,20 +35,14 @@ def test_insert_todo_assigns_sequential_positions(monkeypatch, tmp_path) -> None
3335
assert [task.position for task in tasks] == [0, 1]
3436

3537

36-
def test_get_all_todos_works_with_local_connections(monkeypatch, tmp_path) -> None:
37-
_use_temp_database(monkeypatch, tmp_path)
38-
39-
_ensure_initialized()
38+
def test_get_all_todos_works_with_local_connections(in_memory_database) -> None:
4039
insert_todo(Todo(task="alpha", category="study"))
4140

4241
assert len(get_all_todos()) == 1
4342

4443

45-
def test_get_all_todos_returns_rows_ordered_by_position(monkeypatch, tmp_path) -> None:
46-
_use_temp_database(monkeypatch, tmp_path)
47-
48-
_ensure_initialized()
49-
with get_db_connection() as conn:
44+
def test_get_all_todos_returns_rows_ordered_by_position(in_memory_database) -> None:
45+
with in_memory_database as conn:
5046
conn.execute(
5147
"INSERT INTO todos VALUES (?, ?, ?, ?, ?, ?)",
5248
("second", "work", "", None, 0, 1),
@@ -62,7 +58,9 @@ def test_get_all_todos_returns_rows_ordered_by_position(monkeypatch, tmp_path) -
6258
assert [task.position for task in tasks] == [0, 1]
6359

6460

65-
def test_ensure_initialized_skips_lock_when_already_initialized(monkeypatch, tmp_path) -> None:
61+
def test_ensure_initialized_skips_lock_when_already_initialized(
62+
monkeypatch, tmp_path
63+
) -> None:
6664
_use_temp_database(monkeypatch, tmp_path)
6765
_ensure_initialized()
6866

tests/test_help_docs.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ def _fake_import_module(path: str):
2121
_import_module=_fake_import_module,
2222
)
2323

24+
# Provide a minimal module object with the expected function and
25+
# docstring so `run_help()` can import and print the docstring.
26+
def run_settings():
27+
"""Launch the TruShell settings TUI."""
28+
return None
29+
30+
fake_module = SimpleNamespace(run_settings=run_settings)
31+
fake_kernel._import_module = lambda path: fake_module
32+
2433
monkeypatch.setattr("trushell.core.trukernel.get_kernel", lambda: fake_kernel)
2534

2635
run_help("settings")

tests/test_tasks.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from __future__ import annotations
2+
3+
from typing import Any
4+
5+
6+
def test_update_task_changes_text_single_word(monkeypatch) -> None:
7+
"""Existing parser handles single-word task updates correctly."""
8+
from trushell.commands.tasks import update_task
9+
10+
captured: dict[str, Any] = {}
11+
12+
def fake_update_todo(index: int, task: str | None, category: str | None) -> None:
13+
captured["index"] = index
14+
captured["task"] = task
15+
captured["category"] = category
16+
17+
monkeypatch.setattr("trushell.commands.tasks.update_todo", fake_update_todo)
18+
19+
update_task("1 NewTask")
20+
21+
assert captured["index"] == 0
22+
assert captured["task"] == "NewTask"
23+
assert captured["category"] is None
24+
25+
26+
def test_update_task_multiword_known_bug(monkeypatch) -> None:
27+
"""Pin the current (known) parsing bug for quoted multi-word values.
28+
29+
The current implementation splits with `maxsplit=2` and then strips
30+
surrounding quotes from individual parts. This results in
31+
`updatetask 1 "New text"` producing `task == "New"` and
32+
`category == "text"`. The behaviour is preserved here so the
33+
regression is detected if/fwhen the parsing is fixed.
34+
"""
35+
from trushell.commands.tasks import update_task
36+
37+
captured: dict[str, Any] = {}
38+
39+
def fake_update_todo(index: int, task: str | None, category: str | None) -> None:
40+
captured["index"] = index
41+
captured["task"] = task
42+
captured["category"] = category
43+
44+
monkeypatch.setattr("trushell.commands.tasks.update_todo", fake_update_todo)
45+
46+
update_task('1 "New text"')
47+
48+
assert captured["index"] == 0
49+
# The current broken behaviour yields only the first word as the task
50+
assert captured["task"] == "New"
51+
# and treats the remainder as the category
52+
assert captured["category"] == "text"

trushell/cli.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,16 @@
2525

2626

2727
def app_with_lower() -> None:
28-
"""Entry point that normalizes the first argument to lowercase for case-insensitive invocation."""
29-
# Create a local copy to avoid mutating the global sys.argv
30-
argv = sys.argv.copy()
31-
if len(argv) > 1:
32-
if argv[1].lower() not in {"--help", "-h", "version"}:
33-
raw = " ".join(argv[1:]).lower()
28+
"""Normalize command names without changing the caller's argument list."""
29+
if len(sys.argv) > 1:
30+
argv_copy = sys.argv.copy()
31+
if argv_copy[1].lower() not in {"--help", "-h", "version"}:
32+
first = argv_copy[1].lower()
33+
rest = argv_copy[2:]
34+
raw = " ".join([first] + rest)
3435
get_kernel().execute_command(raw)
3536
return
3637

37-
if argv != sys.argv:
38-
sys.argv = argv
3938
app()
4039

4140

@@ -114,7 +113,7 @@ def action_quit_app(self) -> None:
114113

115114

116115
def _run_external_command(
117-
command: str,
116+
command: str | list[str],
118117
shell: bool = True,
119118
check: bool = False,
120119
cwd: str | None = None,
@@ -302,8 +301,13 @@ def _handle_os_fallback(raw_command: str) -> bool:
302301
return False
303302

304303
try:
305-
completed = _run_external_command(command, shell=True, check=False, cwd=os.getcwd())
306-
except (OSError, subprocess.SubprocessError) as error:
304+
completed = _run_external_command(
305+
shlex.split(command),
306+
shell=False,
307+
check=False,
308+
cwd=os.getcwd(),
309+
)
310+
except (OSError, subprocess.SubprocessError, ValueError) as error:
307311
typer.secho("❓ Command not recognized by TruShell or your host OS.", fg=typer.colors.YELLOW)
308312
typer.secho(f"OS fallback error: {error}", fg=typer.colors.RED)
309313
return True

trushell/commands/data.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,7 @@ def run_csv_view(args: str) -> str:
3636
dialect = csv.Sniffer().sniff(sample, delimiters=",\t")
3737
except csv.Error:
3838
delimiter = "\t" if "\t" in sample else ","
39-
dialect = csv.get_dialect("excel")
40-
dialect.delimiter = delimiter
39+
dialect = csv.excel_tab if delimiter == "\t" else csv.excel
4140

4241
reader = csv.reader(handle, dialect)
4342
headers = next(reader, None)

0 commit comments

Comments
 (0)