feat(mcp): Add reopen command support for closed issues
Implements the `bd` reopen command across the entire MCP stack, enabling agents to reopen closed issues with optional reason tracking for audit trails. This addresses the need to handle regressions and incorrectly closed issues without manual `bd` CLI intervention. The reopen command is more explicit than `bd update --status open` and emits a dedicated Reopened event in the audit log, making it easier to track why issues were reopened during analysis. Changes: - `models.py`: Add ReopenIssueParams with issue_ids list and optional reason - `bd_client.py`: Implement reopen() method with JSON response parsing - `tools.py`: Add beads_reopen_issue() wrapper with Annotated types for MCP - `server.py`: Register 'reopen' MCP tool with description and parameters Testing (10 new): - `test_bd_client.py`: 4 unit tests (mocked subprocess) - `test_bd_client_integration.py`: 3 integration tests (real `bd` CLI) - `test_mcp_server_integration.py`: 3 MCP integration tests (FastMCP Client) - `test_tools.py`: 3 tools wrapper tests (mocked BdClient) Also updated `README.md`.
This commit is contained in:
committed by
Steve Yegge
parent
331a435418
commit
32a718dacd
@@ -12,6 +12,7 @@ from beads_mcp.models import (
|
||||
CreateIssueParams,
|
||||
ListIssuesParams,
|
||||
ReadyWorkParams,
|
||||
ReopenIssueParams,
|
||||
ShowIssueParams,
|
||||
UpdateIssueParams,
|
||||
)
|
||||
@@ -437,6 +438,110 @@ async def test_close_invalid_response(bd_client, mock_process):
|
||||
await bd_client.close(params)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reopen(bd_client, mock_process):
|
||||
"""Test reopen method."""
|
||||
issues_data = [
|
||||
{
|
||||
"id": "bd-1",
|
||||
"title": "Reopened issue",
|
||||
"status": "open",
|
||||
"priority": 1,
|
||||
"issue_type": "bug",
|
||||
"created_at": "2025-01-25T00:00:00Z",
|
||||
"updated_at": "2025-01-25T02:00:00Z",
|
||||
"closed_at": None,
|
||||
}
|
||||
]
|
||||
mock_process.communicate = AsyncMock(return_value=(json.dumps(issues_data).encode(), b""))
|
||||
|
||||
with patch("asyncio.create_subprocess_exec", return_value=mock_process):
|
||||
params = ReopenIssueParams(issue_ids=["bd-1"])
|
||||
issues = await bd_client.reopen(params)
|
||||
|
||||
assert len(issues) == 1
|
||||
assert issues[0].id == "bd-1"
|
||||
assert issues[0].status == "open"
|
||||
assert issues[0].closed_at is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reopen_multiple_issues(bd_client, mock_process):
|
||||
"""Test reopen method with multiple issues."""
|
||||
issues_data = [
|
||||
{
|
||||
"id": "bd-1",
|
||||
"title": "Reopened issue 1",
|
||||
"status": "open",
|
||||
"priority": 1,
|
||||
"issue_type": "bug",
|
||||
"created_at": "2025-01-25T00:00:00Z",
|
||||
"updated_at": "2025-01-25T02:00:00Z",
|
||||
"closed_at": None,
|
||||
},
|
||||
{
|
||||
"id": "bd-2",
|
||||
"title": "Reopened issue 2",
|
||||
"status": "open",
|
||||
"priority": 2,
|
||||
"issue_type": "feature",
|
||||
"created_at": "2025-01-25T00:00:00Z",
|
||||
"updated_at": "2025-01-25T02:00:00Z",
|
||||
"closed_at": None,
|
||||
},
|
||||
]
|
||||
mock_process.communicate = AsyncMock(return_value=(json.dumps(issues_data).encode(), b""))
|
||||
|
||||
with patch("asyncio.create_subprocess_exec", return_value=mock_process):
|
||||
params = ReopenIssueParams(issue_ids=["bd-1", "bd-2"])
|
||||
issues = await bd_client.reopen(params)
|
||||
|
||||
assert len(issues) == 2
|
||||
assert issues[0].id == "bd-1"
|
||||
assert issues[1].id == "bd-2"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reopen_with_reason(bd_client, mock_process):
|
||||
"""Test reopen method with reason parameter."""
|
||||
issues_data = [
|
||||
{
|
||||
"id": "bd-1",
|
||||
"title": "Reopened with reason",
|
||||
"status": "open",
|
||||
"priority": 1,
|
||||
"issue_type": "bug",
|
||||
"created_at": "2025-01-25T00:00:00Z",
|
||||
"updated_at": "2025-01-25T02:00:00Z",
|
||||
"closed_at": None,
|
||||
}
|
||||
]
|
||||
mock_process.communicate = AsyncMock(return_value=(json.dumps(issues_data).encode(), b""))
|
||||
|
||||
with patch("asyncio.create_subprocess_exec", return_value=mock_process):
|
||||
params = ReopenIssueParams(issue_ids=["bd-1"], reason="Found regression")
|
||||
issues = await bd_client.reopen(params)
|
||||
|
||||
assert len(issues) == 1
|
||||
assert issues[0].id == "bd-1"
|
||||
assert issues[0].status == "open"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reopen_invalid_response(bd_client, mock_process):
|
||||
"""Test reopen method with invalid response type."""
|
||||
mock_process.communicate = AsyncMock(
|
||||
return_value=(json.dumps({"error": "not a list"}).encode(), b"")
|
||||
)
|
||||
|
||||
with (
|
||||
patch("asyncio.create_subprocess_exec", return_value=mock_process),
|
||||
pytest.raises(BdCommandError, match="Invalid response for reopen"),
|
||||
):
|
||||
params = ReopenIssueParams(issue_ids=["bd-1"])
|
||||
await bd_client.reopen(params)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_dependency(bd_client, mock_process):
|
||||
"""Test add_dependency method."""
|
||||
|
||||
Reference in New Issue
Block a user