Spaces:
Build error
Build error
Gyeonghun Park Claude Sonnet 4.6 commited on
Commit ·
0702210
1
Parent(s): 406a299
fix(learn): handle FileNotFoundError when CLI tool is not installed
Browse filesWhen --model codex-cli is used but codex is not in PATH,
subprocess.run raises FileNotFoundError. Catch it and raise
a clear RuntimeError with guidance.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
headroom/learn/analyzer.py
CHANGED
|
@@ -361,7 +361,7 @@ def _call_cli_llm(digest: str, model: str) -> dict:
|
|
| 361 |
|
| 362 |
Raises:
|
| 363 |
ValueError: If *model* is not a known CLI backend.
|
| 364 |
-
RuntimeError: If the CLI
|
| 365 |
"""
|
| 366 |
cmd: list[str] | None = None
|
| 367 |
for _name, model_name, cmd_parts in _CLI_BACKENDS:
|
|
@@ -381,6 +381,11 @@ def _call_cli_llm(digest: str, model: str) -> dict:
|
|
| 381 |
text=True,
|
| 382 |
timeout=_CLI_TIMEOUT,
|
| 383 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 384 |
except subprocess.TimeoutExpired:
|
| 385 |
raise RuntimeError(
|
| 386 |
f"`{' '.join(cmd)}` did not respond within {_CLI_TIMEOUT}s. "
|
|
|
|
| 361 |
|
| 362 |
Raises:
|
| 363 |
ValueError: If *model* is not a known CLI backend.
|
| 364 |
+
RuntimeError: If the CLI is not installed, exits non-zero, or times out.
|
| 365 |
"""
|
| 366 |
cmd: list[str] | None = None
|
| 367 |
for _name, model_name, cmd_parts in _CLI_BACKENDS:
|
|
|
|
| 381 |
text=True,
|
| 382 |
timeout=_CLI_TIMEOUT,
|
| 383 |
)
|
| 384 |
+
except FileNotFoundError:
|
| 385 |
+
raise RuntimeError(
|
| 386 |
+
f"`{cmd[0]}` not found in PATH. Install it or use a different backend "
|
| 387 |
+
"with --model <litellm-model-name>."
|
| 388 |
+
) from None
|
| 389 |
except subprocess.TimeoutExpired:
|
| 390 |
raise RuntimeError(
|
| 391 |
f"`{' '.join(cmd)}` did not respond within {_CLI_TIMEOUT}s. "
|
tests/test_learn/test_analyzer.py
CHANGED
|
@@ -547,6 +547,12 @@ class TestCallCliLlm:
|
|
| 547 |
result = _call_cli_llm("test digest", "claude-cli")
|
| 548 |
assert result == {"context_file_rules": [], "memory_file_rules": []}
|
| 549 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 550 |
@patch("headroom.learn.analyzer.subprocess.run")
|
| 551 |
def test_timeout_raises_runtime_error(self, mock_run: MagicMock):
|
| 552 |
mock_run.side_effect = subprocess.TimeoutExpired(cmd=["claude", "-p"], timeout=120)
|
|
|
|
| 547 |
result = _call_cli_llm("test digest", "claude-cli")
|
| 548 |
assert result == {"context_file_rules": [], "memory_file_rules": []}
|
| 549 |
|
| 550 |
+
@patch("headroom.learn.analyzer.subprocess.run")
|
| 551 |
+
def test_cli_not_installed_raises(self, mock_run: MagicMock):
|
| 552 |
+
mock_run.side_effect = FileNotFoundError("No such file or directory: 'codex'")
|
| 553 |
+
with pytest.raises(RuntimeError, match="not found in PATH"):
|
| 554 |
+
_call_cli_llm("test digest", "codex-cli")
|
| 555 |
+
|
| 556 |
@patch("headroom.learn.analyzer.subprocess.run")
|
| 557 |
def test_timeout_raises_runtime_error(self, mock_run: MagicMock):
|
| 558 |
mock_run.side_effect = subprocess.TimeoutExpired(cmd=["claude", "-p"], timeout=120)
|