/ tests / claude_code / test_cli.py
test_cli.py
  1  import json
  2  from pathlib import Path
  3  from unittest import mock
  4  
  5  import pytest
  6  from click.testing import CliRunner
  7  
  8  from mlflow.claude_code.cli import commands
  9  from mlflow.claude_code.config import HOOK_FIELD_COMMAND, HOOK_FIELD_HOOKS
 10  from mlflow.claude_code.hooks import upsert_hook
 11  
 12  
 13  @pytest.fixture
 14  def runner():
 15      """Provide a CLI runner for tests."""
 16      return CliRunner()
 17  
 18  
 19  def test_claude_help_command(runner):
 20      result = runner.invoke(commands, ["--help"])
 21      assert result.exit_code == 0
 22      assert "Commands for autologging with MLflow" in result.output
 23      assert "claude" in result.output
 24  
 25  
 26  def test_trace_command_help(runner):
 27      result = runner.invoke(commands, ["claude", "--help"])
 28      assert result.exit_code == 0
 29      assert "Set up Claude Code tracing" in result.output
 30      assert "--tracking-uri" in result.output
 31      assert "--experiment-id" in result.output
 32      assert "--disable" in result.output
 33      assert "--status" in result.output
 34  
 35  
 36  def test_trace_status_with_no_config(runner):
 37      with runner.isolated_filesystem():
 38          result = runner.invoke(commands, ["claude", "--status"])
 39          assert result.exit_code == 0
 40          assert "❌ Claude tracing is not enabled" in result.output
 41  
 42  
 43  def test_trace_disable_with_no_config(runner):
 44      with runner.isolated_filesystem():
 45          result = runner.invoke(commands, ["claude", "--disable"])
 46          assert result.exit_code == 0
 47  
 48  
 49  def _get_hook_command_from_settings() -> str:
 50      settings_path = Path(".claude/settings.json")
 51      with open(settings_path) as f:
 52          config = json.load(f)
 53  
 54      if hooks := config.get("hooks"):
 55          for group in hooks.get("Stop", []):
 56              for hook in group.get("hooks", []):
 57                  if command := hook.get("command"):
 58                      return command
 59  
 60      raise AssertionError("No hook command found in settings.json")
 61  
 62  
 63  def test_claude_setup_with_uv_env_var(runner, monkeypatch):
 64      monkeypatch.setenv("UV", "/path/to/uv")
 65  
 66      with runner.isolated_filesystem():
 67          result = runner.invoke(commands, ["claude"])
 68          assert result.exit_code == 0
 69  
 70          hook_command = _get_hook_command_from_settings()
 71          assert hook_command == "uv run mlflow autolog claude stop-hook"
 72  
 73  
 74  def test_claude_setup_without_uv_env_var(runner, monkeypatch):
 75      monkeypatch.delenv("UV", raising=False)
 76  
 77      with runner.isolated_filesystem():
 78          result = runner.invoke(commands, ["claude"])
 79          assert result.exit_code == 0
 80  
 81          hook_command = _get_hook_command_from_settings()
 82          assert hook_command == "mlflow autolog claude stop-hook"
 83  
 84  
 85  def test_upsert_hook_uses_cli_command():
 86      config = {HOOK_FIELD_HOOKS: {}}
 87      upsert_hook(config, "Stop", "stop-hook")
 88  
 89      hook_command = config[HOOK_FIELD_HOOKS]["Stop"][0][HOOK_FIELD_HOOKS][0][HOOK_FIELD_COMMAND]
 90      assert "mlflow autolog claude stop-hook" in hook_command
 91  
 92  
 93  def test_upsert_hook_upgrades_legacy_hook():
 94      legacy_command = (
 95          'python -I -c "from mlflow.claude_code.hooks import stop_hook_handler; stop_hook_handler()"'
 96      )
 97      config = {
 98          HOOK_FIELD_HOOKS: {
 99              "Stop": [{HOOK_FIELD_HOOKS: [{"type": "command", HOOK_FIELD_COMMAND: legacy_command}]}]
100          }
101      }
102      upsert_hook(config, "Stop", "stop-hook")
103  
104      hook_command = config[HOOK_FIELD_HOOKS]["Stop"][0][HOOK_FIELD_HOOKS][0][HOOK_FIELD_COMMAND]
105      assert "mlflow autolog claude stop-hook" in hook_command
106      assert "python -I -c" not in hook_command
107  
108  
109  def test_stop_hook_subcommand_is_routable(runner):
110      with mock.patch("mlflow.claude_code.cli.stop_hook_handler") as mock_handler:
111          result = runner.invoke(commands, ["claude", "stop-hook"])
112          assert result.exit_code == 0
113          mock_handler.assert_called_once()