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 files

When --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 exits with a non-zero code or times out.
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)