1+ """Tests for check_tool() — Claude Code CLI detection across install methods.
2+
3+ Covers issue https://github.com/github/spec-kit/issues/550:
4+ `specify check` reports "Claude Code CLI (not found)" even when claude is
5+ installed via npm-local (the default `claude` installer path).
6+ """
7+
8+ from pathlib import Path
9+ from unittest .mock import patch , MagicMock
10+
11+ import pytest
12+
13+ from specify_cli import check_tool
14+
15+
16+ class TestCheckToolClaude :
17+ """Claude CLI detection must work for all install methods."""
18+
19+ def test_detected_via_migrate_installer_path (self , tmp_path ):
20+ """claude migrate-installer puts binary at ~/.claude/local/claude."""
21+ fake_claude = tmp_path / "claude"
22+ fake_claude .touch ()
23+
24+ with patch ("specify_cli.CLAUDE_LOCAL_PATH" , fake_claude ), \
25+ patch ("shutil.which" , return_value = None ):
26+ assert check_tool ("claude" ) is True
27+
28+ def test_detected_via_npm_local_path (self , tmp_path ):
29+ """npm-local install puts binary at ~/.claude/local/node_modules/.bin/claude."""
30+ fake_npm_claude = tmp_path / "node_modules" / ".bin" / "claude"
31+ fake_npm_claude .parent .mkdir (parents = True )
32+ fake_npm_claude .touch ()
33+
34+ # Neither the migrate-installer path nor PATH has claude
35+ fake_migrate = tmp_path / "nonexistent" / "claude"
36+
37+ with patch ("specify_cli.CLAUDE_LOCAL_PATH" , fake_migrate ), \
38+ patch ("specify_cli.CLAUDE_NPM_LOCAL_PATH" , fake_npm_claude ), \
39+ patch ("shutil.which" , return_value = None ):
40+ assert check_tool ("claude" ) is True
41+
42+ def test_detected_via_path (self ):
43+ """claude on PATH (global npm install) should still work."""
44+ fake_missing = Path ("/nonexistent/claude" )
45+
46+ with patch ("specify_cli.CLAUDE_LOCAL_PATH" , fake_missing ), \
47+ patch ("specify_cli.CLAUDE_NPM_LOCAL_PATH" , fake_missing ), \
48+ patch ("shutil.which" , return_value = "/usr/local/bin/claude" ):
49+ assert check_tool ("claude" ) is True
50+
51+ def test_not_found_when_nowhere (self ):
52+ """Should return False when claude is genuinely not installed."""
53+ fake_missing = Path ("/nonexistent/claude" )
54+
55+ with patch ("specify_cli.CLAUDE_LOCAL_PATH" , fake_missing ), \
56+ patch ("specify_cli.CLAUDE_NPM_LOCAL_PATH" , fake_missing ), \
57+ patch ("shutil.which" , return_value = None ):
58+ assert check_tool ("claude" ) is False
59+
60+ def test_tracker_updated_on_npm_local_detection (self , tmp_path ):
61+ """StepTracker should be marked 'available' for npm-local installs."""
62+ fake_npm_claude = tmp_path / "node_modules" / ".bin" / "claude"
63+ fake_npm_claude .parent .mkdir (parents = True )
64+ fake_npm_claude .touch ()
65+
66+ fake_missing = Path ("/nonexistent/claude" )
67+ tracker = MagicMock ()
68+
69+ with patch ("specify_cli.CLAUDE_LOCAL_PATH" , fake_missing ), \
70+ patch ("specify_cli.CLAUDE_NPM_LOCAL_PATH" , fake_npm_claude ), \
71+ patch ("shutil.which" , return_value = None ):
72+ result = check_tool ("claude" , tracker = tracker )
73+
74+ assert result is True
75+ tracker .complete .assert_called_once_with ("claude" , "available" )
76+
77+
78+ class TestCheckToolOther :
79+ """Non-Claude tools should be unaffected by the fix."""
80+
81+ def test_git_detected_via_path (self ):
82+ with patch ("shutil.which" , return_value = "/usr/bin/git" ):
83+ assert check_tool ("git" ) is True
84+
85+ def test_missing_tool (self ):
86+ with patch ("shutil.which" , return_value = None ):
87+ assert check_tool ("nonexistent-tool" ) is False
88+
89+ def test_kiro_fallback (self ):
90+ """kiro-cli detection should try both kiro-cli and kiro."""
91+ def fake_which (name ):
92+ return "/usr/bin/kiro" if name == "kiro" else None
93+
94+ with patch ("shutil.which" , side_effect = fake_which ):
95+ assert check_tool ("kiro-cli" ) is True
0 commit comments