Skip to content
Open
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
1 change: 1 addition & 0 deletions graphify/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,7 @@ def _run_cli() -> None:
print(" --contributor \"Name\" tag who added it to the corpus")
print(" --dir <path> target directory (default: ./raw)")
print(" watch <path> watch a folder and rebuild the graph on code changes")
print(" --debounce N seconds to wait after last change before updating (default: 3)")
print(" update <path> re-extract code files and update the graph (no LLM needed)")
print(" --force overwrite graph.json even if the rebuild has fewer nodes")
print(" (also: GRAPHIFY_FORCE=1 env var; use after refactors that delete code)")
Expand Down
49 changes: 47 additions & 2 deletions graphify/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1730,14 +1730,59 @@ def dispatch_command(cmd: str) -> None:
sys.exit(1)

elif cmd == "watch":
watch_path = Path(sys.argv[2]) if len(sys.argv) > 2 else Path(".")
args = sys.argv[2:]
watch_path: Path | None = None
debounce: float | None = None
i = 0
while i < len(args):
a = args[i]
if a == "--debounce":
if i + 1 >= len(args):
print("error: --debounce requires a value", file=sys.stderr)
sys.exit(2)
try:
debounce = float(args[i + 1])
except ValueError:
print(f"error: invalid debounce value: {args[i + 1]}", file=sys.stderr)
sys.exit(2)
if debounce < 0:
print("error: debounce must be non-negative", file=sys.stderr)
sys.exit(2)
i += 2
continue
if a.startswith("--debounce="):
val = a.split("=", 1)[1]
try:
debounce = float(val)
except ValueError:
print(f"error: invalid debounce value: {val}", file=sys.stderr)
sys.exit(2)
if debounce < 0:
print("error: debounce must be non-negative", file=sys.stderr)
sys.exit(2)
i += 1
continue
if a.startswith("-"):
print(f"error: unknown watch option: {a}", file=sys.stderr)
sys.exit(2)
if watch_path is not None:
print("error: watch accepts at most one path argument", file=sys.stderr)
sys.exit(2)
watch_path = Path(a)
i += 1

if watch_path is None:
watch_path = Path(".")
if not watch_path.exists():
print(f"error: path not found: {watch_path}", file=sys.stderr)
sys.exit(1)
from graphify.watch import watch as _watch

try:
_watch(watch_path)
if debounce is not None:
_watch(watch_path, debounce=debounce)
else:
_watch(watch_path)
except ImportError as exc:
print(f"error: {exc}", file=sys.stderr)
sys.exit(1)
Expand Down
79 changes: 79 additions & 0 deletions tests/test_cli_watch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""#3061: graphify watch must parse --debounce and forward it to watch()."""
from __future__ import annotations

import sys
from unittest.mock import patch

import pytest

from graphify.cli import dispatch_command

PYTHON = sys.executable


def _run_watch(argv_tail: list[str], cwd: str) -> object:
"""Invoke dispatch_command('watch') with a patched watch(); cwd must exist."""
with patch("graphify.watch.watch") as mock_watch:
old_argv = sys.argv
sys.argv = ["graphify", "watch"] + argv_tail
try:
dispatch_command("watch")
finally:
sys.argv = old_argv
return mock_watch


def test_watch_debounce_space_form(tmp_path):
mock = _run_watch(["--debounce", "60", str(tmp_path)], str(tmp_path))
mock.assert_called_once_with(tmp_path, debounce=60.0)


def test_watch_debounce_equals_form(tmp_path):
mock = _run_watch(["--debounce=60", str(tmp_path)], str(tmp_path))
mock.assert_called_once_with(tmp_path, debounce=60.0)


def test_watch_debounce_zero_allowed(tmp_path):
mock = _run_watch(["--debounce", "0", str(tmp_path)], str(tmp_path))
mock.assert_called_once_with(tmp_path, debounce=0.0)


def test_watch_without_debounce_uses_default_kwarg(tmp_path):
mock = _run_watch([str(tmp_path)], str(tmp_path))
mock.assert_called_once_with(tmp_path)


def test_watch_debounce_missing_value(tmp_path):
with pytest.raises(SystemExit) as exc:
_run_watch(["--debounce", str(tmp_path)], str(tmp_path))
assert exc.value.code == 2


def test_watch_debounce_negative_rejected(tmp_path):
with pytest.raises(SystemExit) as exc:
_run_watch(["--debounce", "-1", str(tmp_path)], str(tmp_path))
assert exc.value.code == 2


def test_watch_debounce_non_numeric_rejected(tmp_path):
with pytest.raises(SystemExit) as exc:
_run_watch(["--debounce", "foo", str(tmp_path)], str(tmp_path))
assert exc.value.code == 2


def test_watch_unknown_option_rejected(tmp_path):
with pytest.raises(SystemExit) as exc:
_run_watch(["--bogus", str(tmp_path)], str(tmp_path))
assert exc.value.code == 2


def test_watch_help_lists_debounce():
import subprocess

r = subprocess.run(
[PYTHON, "-m", "graphify", "--help"],
capture_output=True,
text=True,
)
assert r.returncode == 0
assert "--debounce" in r.stdout
Loading