fix(cli,mine): shell-quote project_dir in resume hints
The "Skipped. Run mempalace mine <dir>" hint after declining the init prompt and the "Re-run mempalace mine <dir> to resume" hint after a Ctrl-C interruption both interpolated project_dir without shell-quoting. A path containing spaces or metacharacters produced a copy-paste-broken command. Both spots now use shlex.quote(project_dir). Adds regression tests covering each hint with a path that contains a space.
This commit is contained in:
+1
-1
@@ -238,7 +238,7 @@ def _maybe_run_mine_after_init(args, cfg) -> None:
|
|||||||
# we don't block. User can re-run with --auto-mine to opt in.
|
# we don't block. User can re-run with --auto-mine to opt in.
|
||||||
answer = "n"
|
answer = "n"
|
||||||
if answer not in ("", "y", "yes"):
|
if answer not in ("", "y", "yes"):
|
||||||
print(f"\n Skipped. Run `mempalace mine {project_dir}` when ready.")
|
print(f"\n Skipped. Run `mempalace mine {shlex.quote(project_dir)}` when ready.")
|
||||||
return
|
return
|
||||||
|
|
||||||
palace_path = cfg.palace_path
|
palace_path = cfg.palace_path
|
||||||
|
|||||||
+2
-1
@@ -9,6 +9,7 @@ Stores verbatim chunks as drawers. No summaries. Ever.
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import shlex
|
||||||
import hashlib
|
import hashlib
|
||||||
import fnmatch
|
import fnmatch
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -1103,7 +1104,7 @@ def mine(
|
|||||||
print(f" drawers_filed: {total_drawers}")
|
print(f" drawers_filed: {total_drawers}")
|
||||||
print(f" last_file: {last_file or '<none>'}")
|
print(f" last_file: {last_file or '<none>'}")
|
||||||
print(
|
print(
|
||||||
f"\n Re-run `mempalace mine {project_dir}` to resume — "
|
f"\n Re-run `mempalace mine {shlex.quote(project_dir)}` to resume — "
|
||||||
"already-filed drawers are\n upserted idempotently and will not duplicate.\n"
|
"already-filed drawers are\n upserted idempotently and will not duplicate.\n"
|
||||||
)
|
)
|
||||||
sys.exit(130)
|
sys.exit(130)
|
||||||
|
|||||||
@@ -281,6 +281,29 @@ def test_maybe_run_mine_yes_and_auto_mine_fully_noninteractive(tmp_path):
|
|||||||
mock_mine.assert_called_once()
|
mock_mine.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_maybe_run_mine_decline_quotes_path_with_spaces(tmp_path, capsys):
|
||||||
|
"""The resume hint must shell-quote the project dir so paths with
|
||||||
|
spaces / metacharacters produce a copy-paste-safe command."""
|
||||||
|
from mempalace.cli import _maybe_run_mine_after_init
|
||||||
|
|
||||||
|
spaced_dir = tmp_path / "my project dir"
|
||||||
|
spaced_dir.mkdir()
|
||||||
|
args = argparse.Namespace(dir=str(spaced_dir), yes=False, auto_mine=False)
|
||||||
|
cfg = _fake_cfg(tmp_path)
|
||||||
|
with (
|
||||||
|
patch("mempalace.miner.mine"),
|
||||||
|
patch("mempalace.miner.scan_project", return_value=[]),
|
||||||
|
patch("builtins.input", return_value="n"),
|
||||||
|
):
|
||||||
|
_maybe_run_mine_after_init(args, cfg)
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
# shlex.quote wraps paths with spaces in single quotes.
|
||||||
|
assert f"mempalace mine '{spaced_dir}'" in out
|
||||||
|
# And the bare unquoted form is NOT printed (would break paste).
|
||||||
|
assert f"mempalace mine {spaced_dir} " not in out
|
||||||
|
assert f"mempalace mine {spaced_dir}`" not in out
|
||||||
|
|
||||||
|
|
||||||
def test_maybe_run_mine_eof_on_stdin_treated_as_decline(tmp_path, capsys):
|
def test_maybe_run_mine_eof_on_stdin_treated_as_decline(tmp_path, capsys):
|
||||||
"""Piped / non-interactive stdin (EOFError) declines without crashing."""
|
"""Piped / non-interactive stdin (EOFError) declines without crashing."""
|
||||||
from mempalace.cli import _maybe_run_mine_after_init
|
from mempalace.cli import _maybe_run_mine_after_init
|
||||||
|
|||||||
@@ -653,6 +653,30 @@ def test_mine_keyboard_interrupt_prints_summary_and_exits_130(tmp_path, capsys):
|
|||||||
assert "upserted idempotently" in out
|
assert "upserted idempotently" in out
|
||||||
|
|
||||||
|
|
||||||
|
def test_mine_keyboard_interrupt_quotes_path_with_spaces_in_resume_hint(tmp_path, capsys):
|
||||||
|
"""Resume hint must shell-quote the project dir so a path containing
|
||||||
|
spaces / metacharacters yields a copy-paste-safe `mempalace mine ...`
|
||||||
|
command. Otherwise users on a path like "My Project" hit a broken
|
||||||
|
invocation when they re-run after Ctrl-C."""
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
project_root = tmp_path / "my project"
|
||||||
|
project_root.mkdir()
|
||||||
|
_make_minable_project(project_root, n_files=2)
|
||||||
|
palace_path = project_root / "palace"
|
||||||
|
|
||||||
|
def fake_process_file(*args, **kwargs):
|
||||||
|
raise KeyboardInterrupt
|
||||||
|
|
||||||
|
with patch("mempalace.miner.process_file", side_effect=fake_process_file):
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
mine(str(project_root), str(palace_path))
|
||||||
|
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert f"mempalace mine '{project_root}'" in out
|
||||||
|
|
||||||
|
|
||||||
def test_mine_cleans_up_pid_file_on_interrupt(tmp_path):
|
def test_mine_cleans_up_pid_file_on_interrupt(tmp_path):
|
||||||
"""Our own PID entry in mine.pid is removed in the finally clause."""
|
"""Our own PID entry in mine.pid is removed in the finally clause."""
|
||||||
import pytest
|
import pytest
|
||||||
|
|||||||
Reference in New Issue
Block a user