14fbc2ebb8
* fix(gh-cli): use get_gh_executable() and pass GITHUB_CLI_PATH from GUI
Frontend changes:
- Add GITHUB_CLI_PATH environment variable detection in agent-process.ts
- Follow the existing CLAUDE_CLI_PATH pattern for gh CLI path passing
- Resolves issue where GUI (from Finder/Dock) doesn't have gh in PATH
Backend changes:
- gh_client.py: Use get_gh_executable() instead of hardcoded "gh"
- bot_detection.py: Use get_gh_executable() in _get_bot_username()
- runner.py: Use get_gh_executable() instead of shutil.which() duplication
This fix ensures gh CLI is found when the Electron app is launched from
Finder/Dock on macOS, or from non-terminal environments on Windows/Linux,
where the subprocess PATH doesn't include Homebrew or other custom install
locations.
The get_gh_executable() function already handles:
- GITHUB_CLI_PATH env var (now set by frontend)
- shutil.which("gh") fallback
- Platform-specific paths (Homebrew, Program Files)
- Windows 'where' command
Resolves: ACS-321
* tests: add comprehensive tests for gh CLI path detection (ACS-321)
Backend tests:
- Add tests/test_gh_executable.py with 28 tests covering:
- gh executable verification with --version check
- cache invalidation functionality
- Windows 'where' command fallback
- cross-platform path detection (Homebrew, Program Files)
- run_gh() command execution wrapper
- Add tests for gh_cli_path detection in:
- apps/backend/runners/github/test_gh_client.py (3 new tests)
- apps/backend/runners/github/test_bot_detection.py (6 new tests)
Frontend tests:
- Add tests for GITHUB_CLI_PATH env var in:
- apps/frontend/src/main/agent/agent-process.test.ts (6 new tests)
- Fix flaky subprocess-spawn.test.ts timeout issue by increasing timeout
to 10 seconds for the spec creation test
- Fix TypeScript type errors in test mocks to match ToolDetectionResult type
All 43 new tests pass:
- 28/28 tests in test_gh_executable.py
- 3/3 new tests in test_gh_client.py
- 6/6 new tests in test_bot_detection.py
- 6/6 new tests in agent-process.test.ts
Resolves: ACS-321 test coverage
* fix(tests): improve test assertions and remove invalid noqa comment
- Remove invalid noqa: PY291 (not a valid ruff rule for CodeQL)
- Remove unused result variables
- Properly patch get_gh_executable in test_run_with_github_cli_path_env_var
- Add assertion to verify env var path was actually used
* fix(tests): fix CodeQL and test assertion issues
- Fix HIGH: Test assertion bug in test_bot_detection.py line 455 - was comparing list to string
- Fix MEDIUM: Add GITHUB_CLI_PATH verification in test_get_bot_username_uses_github_cli_path_env_var
- Fix MEDIUM: Remove tautological test_run_with_github_cli_path_env_var from test_gh_client.py
- test_gh_executable.py already has proper env var precedence tests
* fix(tests): emit exit synchronously to fix Windows CI timeouts
- Change from setImmediate to synchronous mockProcess.emit('exit', 0)
- This ensures the exit event fires before the await, preventing timeouts
- setImmediate was too slow on Windows CI, causing test failures
* style(tests): remove unused AsyncMock import
* refactor(agent): extract detectAndSetCliPath helper to reduce duplication
- Extracts common CLI path detection pattern into detectAndSetCliPath()
- Reduces code duplication between CLAUDE_CLI_PATH and GITHUB_CLI_PATH detection
- Both now use the same helper function with tool name and env var parameters
- Improves maintainability - future CLI tools can reuse this pattern
Suggested by code review (LOW priority).
* test(tests): improve test clarity and add subprocess call verification
- Rename test_get_bot_username_uses_github_cli_path_env_var to
test_get_bot_username_uses_get_gh_executable_return_value for clarity
- Remove redundant GH_TOKEN monkeypatch (BotDetector sets it from bot_token)
- Add assertion for full command args including ['api', 'user']
- Add subprocess.run assertion to test_get_bot_username_without_token
to verify no gh CLI invocation occurs when no token is provided
Suggested by code review.
* fix(tests): fix "should track running tasks" test timing
The test was failing because it emitted exit events before the spawn
async operations had completed and registered exit listeners. Using
vi.waitFor() ensures tasks are tracked before emitting exit.
* fix(tests): address code review feedback and Windows CI timeout
- [NEW-001] Add MOCK_GH_PATH constant in TestGhExecutableDetection class
to avoid hardcoded Unix-style paths in test_bot_detection.py
- [NEW-002] Improve error message serialization in agent-process.ts
detectAndSetCliPath() to properly extract error.message from Error objects
- Fix Windows CI timeout: Increase test timeouts from 10000ms to 15000ms
for spec creation, task execution, and QA process tests
* docs: add note about pre-existing test failures
Add comment to clarify that some pre-existing test failures in the
full test suite (e.g., @testing-library/react v16 exports) are not
related to changes in this test file.
* fix(tests): ensure exit listeners are attached before emitting exit
Use setImmediate to wait for spawn to complete before emitting exit events.
This prevents flaky timeouts where exit fires before listeners are registered.
Addresses feedback about emitting exit before spawn listeners are attached.
* fix(tests): rename test to reflect actual behavior
The test 'should kill existing process when starting new one for same task'
was incorrectly named - the agent doesn't implement kill behavior for
duplicate tasks. Renamed to 'should allow sequential execution of same task'
to accurately reflect what the code actually does.
Kill behavior is out of scope for this bug fix (ACS-321).
* refactor(agent-process): make detectAndSetCliPath type-safe
- Add CliTool type and CLI_TOOL_ENV_MAP mapping at module level
- Remove envVarName parameter - now looked up via mapping
- Prevents mismatched toolName and envVarName pairs
- Fixes esbuild compatibility issue with private type syntax
* refactor(tests): use temp directory paths instead of hardcoded Unix paths
Replace MOCK_GH_PATH constant with platform-agnostic temp_state_dir / 'gh'
paths in TestGhExecutableDetection class. This follows cross-platform
guidelines by avoiding hardcoded Unix-style paths in tests.
* refactor(tests): address coderabbitai feedback
- test_bot_detection.py: Remove redundant monkeypatch.setenv() call in
test_get_bot_username_uses_get_gh_executable_return_value since get_gh_executable
is explicitly mocked to return mock_gh_path
- subprocess-spawn.test.ts: Keep original should kill all running tasks test
that matches actual behavior (killAll removes tasks from tracking but doesn't
call kill on already-exited mock processes)
---------
Co-authored-by: StillKnotKnown <stillknotknown@users.noreply.github.com>
Co-authored-by: Andy <119136210+AndyMik90@users.noreply.github.com>
111 lines
3.8 KiB
Python
111 lines
3.8 KiB
Python
"""
|
|
Tests for GHClient timeout and retry functionality.
|
|
"""
|
|
|
|
import asyncio
|
|
from pathlib import Path
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
from gh_client import GHClient, GHCommandError, GHTimeoutError
|
|
|
|
|
|
class TestGHClient:
|
|
"""Test suite for GHClient."""
|
|
|
|
@pytest.fixture
|
|
def client(self, tmp_path):
|
|
"""Create a test client."""
|
|
return GHClient(
|
|
project_dir=tmp_path,
|
|
default_timeout=2.0,
|
|
max_retries=3,
|
|
)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_timeout_raises_error(self, client):
|
|
"""Test that commands timeout after max retries."""
|
|
# Use a command that will timeout (sleep longer than timeout)
|
|
with pytest.raises(GHTimeoutError) as exc_info:
|
|
await client.run(["api", "/repos/nonexistent/repo"], timeout=0.1)
|
|
|
|
assert "timed out after 3 attempts" in str(exc_info.value)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_invalid_command_raises_error(self, client):
|
|
"""Test that invalid commands raise GHCommandError."""
|
|
with pytest.raises(GHCommandError):
|
|
await client.run(["invalid-command"])
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_successful_command(self, client):
|
|
"""Test successful command execution."""
|
|
# This test requires gh CLI to be installed
|
|
try:
|
|
result = await client.run(["--version"])
|
|
assert result.returncode == 0
|
|
assert "gh version" in result.stdout
|
|
assert result.attempts == 1
|
|
except Exception:
|
|
pytest.skip("gh CLI not available")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_convenience_methods_timeout_protection(self, client):
|
|
"""Test that convenience methods have timeout protection."""
|
|
# These will fail because repo doesn't exist, but should not hang
|
|
with pytest.raises((GHCommandError, GHTimeoutError)):
|
|
await client.pr_list()
|
|
|
|
with pytest.raises((GHCommandError, GHTimeoutError)):
|
|
await client.issue_list()
|
|
|
|
|
|
class TestGHClientGhExecutableDetection:
|
|
"""Test suite for GHClient gh executable detection."""
|
|
|
|
@pytest.fixture
|
|
def client(self, tmp_path):
|
|
"""Create a test client."""
|
|
return GHClient(
|
|
project_dir=tmp_path,
|
|
default_timeout=2.0,
|
|
max_retries=3,
|
|
)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_raises_error_when_gh_not_found(self, client):
|
|
"""Test that run() raises GHCommandError when gh is not found."""
|
|
with patch("gh_client.get_gh_executable", return_value=None):
|
|
with pytest.raises(GHCommandError) as exc_info:
|
|
await client.run(["--version"])
|
|
|
|
assert "not found" in str(exc_info.value)
|
|
# Test verifies error message mentions GitHub CLI for user guidance
|
|
assert "GitHub CLI" in str(exc_info.value)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_uses_detected_gh_executable(self, client):
|
|
"""Test that run() uses the detected gh executable path."""
|
|
mock_exec = "/custom/path/to/gh"
|
|
|
|
with patch("gh_client.get_gh_executable", return_value=mock_exec):
|
|
with patch("asyncio.create_subprocess_exec") as mock_subprocess:
|
|
# Mock the subprocess to return immediately
|
|
mock_proc = MagicMock()
|
|
mock_proc.communicate = AsyncMock(
|
|
return_value=(b"gh version 2.0.0\n", b"")
|
|
)
|
|
mock_proc.returncode = 0
|
|
mock_subprocess.return_value = mock_proc
|
|
|
|
await client.run(["--version"])
|
|
|
|
# Verify the correct gh path was used
|
|
mock_subprocess.assert_called_once()
|
|
called_cmd = mock_subprocess.call_args[0][0]
|
|
assert called_cmd == mock_exec
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|