fix(init): split --auto-mine from --yes; show file-count estimate before mine prompt
Reviewer feedback on the previous commit flagged two real problems:
1. Overloading --yes to also auto-mine was a silent behaviour change for
scripted callers. Today --yes only auto-accepts entities — making it
ALSO trigger a multi-minute ChromaDB write breaks every script that
currently runs `mempalace init --yes <dir>` for the fast non-interactive
entity path. Add a separate `--auto-mine` flag instead. Combinations:
mempalace init --yes <dir> # entities auto, STILL prompt mine
mempalace init --auto-mine <dir> # prompt entities, skip mine prompt
mempalace init --yes --auto-mine <dir> # fully non-interactive
--yes behaviour is now identical to pre-PR.
2. The mine prompt was firing without telling the user how big the job
was. On a real corpus mine takes minutes-to-tens-of-minutes; hitting
Enter on default-Y with no size cue is a footgun. Show a one-line
estimate computed from scan_project (the same walk we hand into mine)
BEFORE the prompt:
~423 files (~12 MB) would be mined into this palace.
Mine this directory now? [Y/n]
The estimate uses a single corpus walk: scan_project's output is
passed into mine() via a new optional files= kwarg, so we never walk
the tree twice.
Tests: replaced the old "--yes auto-mines" assertion with a regression
guard that --yes alone STILL prompts; added coverage for --auto-mine
alone, --yes --auto-mine together, and the pre-prompt estimate line.
This commit is contained in:
+101
-13
@@ -152,8 +152,8 @@ def test_cmd_init_with_entities_zero_total(mock_config_cls, tmp_path, capsys):
|
||||
# ── _maybe_run_mine_after_init (init → mine prompt, #1181) ─────────────
|
||||
|
||||
|
||||
def _init_args(tmp_path, *, yes=False):
|
||||
return argparse.Namespace(dir=str(tmp_path), yes=yes)
|
||||
def _init_args(tmp_path, *, yes=False, auto_mine=False):
|
||||
return argparse.Namespace(dir=str(tmp_path), yes=yes, auto_mine=auto_mine)
|
||||
|
||||
|
||||
def _fake_cfg(tmp_path):
|
||||
@@ -162,26 +162,41 @@ def _fake_cfg(tmp_path):
|
||||
return cfg
|
||||
|
||||
|
||||
def _fake_scanned(tmp_path, n=3):
|
||||
"""Build n real Path objects with stat()-able sizes for the scan estimate."""
|
||||
paths = []
|
||||
for i in range(n):
|
||||
p = tmp_path / f"f{i}.txt"
|
||||
p.write_text("x" * 1024) # 1 KB each
|
||||
paths.append(p)
|
||||
return paths
|
||||
|
||||
|
||||
def test_maybe_run_mine_prompt_accepted_runs_mine(tmp_path):
|
||||
"""Empty / 'y' / 'yes' on the prompt triggers mine() in-process."""
|
||||
from mempalace.cli import _maybe_run_mine_after_init
|
||||
|
||||
args = _init_args(tmp_path, yes=False)
|
||||
args = _init_args(tmp_path, yes=False, auto_mine=False)
|
||||
cfg = _fake_cfg(tmp_path)
|
||||
scanned = _fake_scanned(tmp_path, n=3)
|
||||
with (
|
||||
patch("mempalace.miner.mine") as mock_mine,
|
||||
patch("mempalace.miner.scan_project", return_value=["a", "b", "c"]),
|
||||
patch("mempalace.miner.scan_project", return_value=scanned),
|
||||
patch("builtins.input", return_value=""),
|
||||
):
|
||||
_maybe_run_mine_after_init(args, cfg)
|
||||
mock_mine.assert_called_once_with(project_dir=str(tmp_path), palace_path=cfg.palace_path)
|
||||
mock_mine.assert_called_once_with(
|
||||
project_dir=str(tmp_path),
|
||||
palace_path=cfg.palace_path,
|
||||
files=scanned,
|
||||
)
|
||||
|
||||
|
||||
def test_maybe_run_mine_prompt_yes_accepted_runs_mine(tmp_path):
|
||||
"""Explicit 'y' answer also runs mine()."""
|
||||
from mempalace.cli import _maybe_run_mine_after_init
|
||||
|
||||
args = _init_args(tmp_path, yes=False)
|
||||
args = _init_args(tmp_path, yes=False, auto_mine=False)
|
||||
cfg = _fake_cfg(tmp_path)
|
||||
with (
|
||||
patch("mempalace.miner.mine") as mock_mine,
|
||||
@@ -196,7 +211,7 @@ def test_maybe_run_mine_prompt_declined_prints_hint(tmp_path, capsys):
|
||||
"""'n' answer skips mine() and prints the resume hint."""
|
||||
from mempalace.cli import _maybe_run_mine_after_init
|
||||
|
||||
args = _init_args(tmp_path, yes=False)
|
||||
args = _init_args(tmp_path, yes=False, auto_mine=False)
|
||||
cfg = _fake_cfg(tmp_path)
|
||||
with (
|
||||
patch("mempalace.miner.mine") as mock_mine,
|
||||
@@ -210,11 +225,52 @@ def test_maybe_run_mine_prompt_declined_prints_hint(tmp_path, capsys):
|
||||
assert "Skipped" in out
|
||||
|
||||
|
||||
def test_maybe_run_mine_yes_flag_skips_prompt_and_mines(tmp_path):
|
||||
"""`--yes` runs mine() automatically without calling input()."""
|
||||
def test_maybe_run_mine_yes_alone_still_prompts(tmp_path):
|
||||
"""`--yes` is scoped to entity auto-accept and MUST still prompt for mine.
|
||||
|
||||
Regression guard for the flag-overload review feedback on #1183: extending
|
||||
`--yes` to also auto-mine would silently change behaviour for scripted
|
||||
callers and turn a fast command into a minutes-long ChromaDB write.
|
||||
"""
|
||||
from mempalace.cli import _maybe_run_mine_after_init
|
||||
|
||||
args = _init_args(tmp_path, yes=True)
|
||||
args = _init_args(tmp_path, yes=True, auto_mine=False)
|
||||
cfg = _fake_cfg(tmp_path)
|
||||
with (
|
||||
patch("mempalace.miner.mine") as mock_mine,
|
||||
patch("mempalace.miner.scan_project", return_value=[]),
|
||||
patch("builtins.input", return_value="n") as mock_input,
|
||||
):
|
||||
_maybe_run_mine_after_init(args, cfg)
|
||||
mock_input.assert_called_once() # the prompt MUST fire
|
||||
mock_mine.assert_not_called()
|
||||
|
||||
|
||||
def test_maybe_run_mine_auto_mine_skips_prompt(tmp_path):
|
||||
"""`--auto-mine` runs mine() automatically without calling input()."""
|
||||
from mempalace.cli import _maybe_run_mine_after_init
|
||||
|
||||
args = _init_args(tmp_path, yes=False, auto_mine=True)
|
||||
cfg = _fake_cfg(tmp_path)
|
||||
scanned = _fake_scanned(tmp_path, n=2)
|
||||
with (
|
||||
patch("mempalace.miner.mine") as mock_mine,
|
||||
patch("mempalace.miner.scan_project", return_value=scanned),
|
||||
patch("builtins.input", side_effect=AssertionError("input() must not be called")),
|
||||
):
|
||||
_maybe_run_mine_after_init(args, cfg)
|
||||
mock_mine.assert_called_once_with(
|
||||
project_dir=str(tmp_path),
|
||||
palace_path=cfg.palace_path,
|
||||
files=scanned,
|
||||
)
|
||||
|
||||
|
||||
def test_maybe_run_mine_yes_and_auto_mine_fully_noninteractive(tmp_path):
|
||||
"""`--yes --auto-mine` together: never call input(), always mine."""
|
||||
from mempalace.cli import _maybe_run_mine_after_init
|
||||
|
||||
args = _init_args(tmp_path, yes=True, auto_mine=True)
|
||||
cfg = _fake_cfg(tmp_path)
|
||||
with (
|
||||
patch("mempalace.miner.mine") as mock_mine,
|
||||
@@ -222,14 +278,14 @@ def test_maybe_run_mine_yes_flag_skips_prompt_and_mines(tmp_path):
|
||||
patch("builtins.input", side_effect=AssertionError("input() must not be called")),
|
||||
):
|
||||
_maybe_run_mine_after_init(args, cfg)
|
||||
mock_mine.assert_called_once_with(project_dir=str(tmp_path), palace_path=cfg.palace_path)
|
||||
mock_mine.assert_called_once()
|
||||
|
||||
|
||||
def test_maybe_run_mine_eof_on_stdin_treated_as_decline(tmp_path, capsys):
|
||||
"""Piped / non-interactive stdin (EOFError) declines without crashing."""
|
||||
from mempalace.cli import _maybe_run_mine_after_init
|
||||
|
||||
args = _init_args(tmp_path, yes=False)
|
||||
args = _init_args(tmp_path, yes=False, auto_mine=False)
|
||||
cfg = _fake_cfg(tmp_path)
|
||||
with (
|
||||
patch("mempalace.miner.mine") as mock_mine,
|
||||
@@ -245,7 +301,7 @@ def test_maybe_run_mine_failure_surfaces_via_exit(tmp_path, capsys):
|
||||
"""Mine errors are not swallowed — they exit non-zero with an error line."""
|
||||
from mempalace.cli import _maybe_run_mine_after_init
|
||||
|
||||
args = _init_args(tmp_path, yes=True)
|
||||
args = _init_args(tmp_path, yes=False, auto_mine=True)
|
||||
cfg = _fake_cfg(tmp_path)
|
||||
with (
|
||||
patch("mempalace.miner.mine", side_effect=RuntimeError("boom")),
|
||||
@@ -258,6 +314,38 @@ def test_maybe_run_mine_failure_surfaces_via_exit(tmp_path, capsys):
|
||||
assert "boom" in err
|
||||
|
||||
|
||||
def test_maybe_run_mine_estimate_appears_before_prompt(tmp_path, capsys):
|
||||
"""The file-count + size estimate line MUST render BEFORE the prompt.
|
||||
|
||||
Required by the spec: hitting Enter on a default-Y prompt with no size
|
||||
info is a footgun on a real corpus where mine takes minutes. The user
|
||||
must see scope before being asked to confirm.
|
||||
"""
|
||||
from mempalace.cli import _maybe_run_mine_after_init
|
||||
|
||||
args = _init_args(tmp_path, yes=False, auto_mine=False)
|
||||
cfg = _fake_cfg(tmp_path)
|
||||
scanned = _fake_scanned(tmp_path, n=4) # 4 files * 1 KB each
|
||||
captured_when_prompted = {}
|
||||
|
||||
def fake_input(prompt):
|
||||
# Snapshot what stdout looked like at the moment the prompt fires.
|
||||
captured_when_prompted["stdout"] = capsys.readouterr().out
|
||||
return "n"
|
||||
|
||||
with (
|
||||
patch("mempalace.miner.mine"),
|
||||
patch("mempalace.miner.scan_project", return_value=scanned),
|
||||
patch("builtins.input", side_effect=fake_input),
|
||||
):
|
||||
_maybe_run_mine_after_init(args, cfg)
|
||||
|
||||
pre_prompt = captured_when_prompted["stdout"]
|
||||
assert "4 files" in pre_prompt, f"file count missing from pre-prompt output: {pre_prompt!r}"
|
||||
assert "MB" in pre_prompt, f"size estimate missing from pre-prompt output: {pre_prompt!r}"
|
||||
assert "would be mined" in pre_prompt
|
||||
|
||||
|
||||
# ── cmd_mine ───────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user