test(rpc): add blocker check tests for close operation (GH#962)
Add two tests to verify that issue blocking/dependencies are enforced when closing issues via the RPC handler: - TestHandleClose_BlockerCheck: Verifies closing a blocked issue fails when blocker is still open, and --force flag overrides the check - TestHandleClose_BlockerCheck_ClosedBlocker: Verifies close succeeds once the blocking issue is closed Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ package rpc
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -1254,3 +1255,205 @@ func TestHandleUpdate_ClaimFlag_WithOtherUpdates(t *testing.T) {
|
|||||||
t.Errorf("expected priority 0, got %d", issue.Priority)
|
t.Errorf("expected priority 0, got %d", issue.Priority)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestHandleClose_BlockerCheck verifies that close operation checks for open blockers (GH#962)
|
||||||
|
func TestHandleClose_BlockerCheck(t *testing.T) {
|
||||||
|
store := memory.New("/tmp/test.jsonl")
|
||||||
|
server := NewServer("/tmp/test.sock", store, "/tmp", "/tmp/test.db")
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Create two issues: blocker and blocked
|
||||||
|
blockerArgs := CreateArgs{
|
||||||
|
Title: "Blocker Issue",
|
||||||
|
IssueType: "bug",
|
||||||
|
Priority: 1,
|
||||||
|
}
|
||||||
|
blockerJSON, _ := json.Marshal(blockerArgs)
|
||||||
|
blockerReq := &Request{
|
||||||
|
Operation: OpCreate,
|
||||||
|
Args: blockerJSON,
|
||||||
|
Actor: "test-user",
|
||||||
|
}
|
||||||
|
|
||||||
|
blockerResp := server.handleCreate(blockerReq)
|
||||||
|
if !blockerResp.Success {
|
||||||
|
t.Fatalf("failed to create blocker issue: %s", blockerResp.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
var blockerIssue types.Issue
|
||||||
|
if err := json.Unmarshal(blockerResp.Data, &blockerIssue); err != nil {
|
||||||
|
t.Fatalf("failed to parse blocker issue: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
blockedArgs := CreateArgs{
|
||||||
|
Title: "Blocked Issue",
|
||||||
|
IssueType: "task",
|
||||||
|
Priority: 2,
|
||||||
|
}
|
||||||
|
blockedJSON, _ := json.Marshal(blockedArgs)
|
||||||
|
blockedReq := &Request{
|
||||||
|
Operation: OpCreate,
|
||||||
|
Args: blockedJSON,
|
||||||
|
Actor: "test-user",
|
||||||
|
}
|
||||||
|
|
||||||
|
blockedResp := server.handleCreate(blockedReq)
|
||||||
|
if !blockedResp.Success {
|
||||||
|
t.Fatalf("failed to create blocked issue: %s", blockedResp.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
var blockedIssue types.Issue
|
||||||
|
if err := json.Unmarshal(blockedResp.Data, &blockedIssue); err != nil {
|
||||||
|
t.Fatalf("failed to parse blocked issue: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add dependency: blockedIssue depends on blockerIssue (blockerIssue blocks blockedIssue)
|
||||||
|
dep := &types.Dependency{
|
||||||
|
IssueID: blockedIssue.ID,
|
||||||
|
DependsOnID: blockerIssue.ID,
|
||||||
|
Type: types.DepBlocks,
|
||||||
|
}
|
||||||
|
if err := store.AddDependency(ctx, dep, "test-user"); err != nil {
|
||||||
|
t.Fatalf("failed to add dependency: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to close the blocked issue - should FAIL
|
||||||
|
closeArgs := CloseArgs{
|
||||||
|
ID: blockedIssue.ID,
|
||||||
|
Reason: "attempting to close blocked issue",
|
||||||
|
}
|
||||||
|
closeJSON, _ := json.Marshal(closeArgs)
|
||||||
|
closeReq := &Request{
|
||||||
|
Operation: OpClose,
|
||||||
|
Args: closeJSON,
|
||||||
|
Actor: "test-user",
|
||||||
|
}
|
||||||
|
|
||||||
|
closeResp := server.handleClose(closeReq)
|
||||||
|
if closeResp.Success {
|
||||||
|
t.Error("expected close to fail for blocked issue, but it succeeded")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify error message mentions blockers
|
||||||
|
if closeResp.Error == "" {
|
||||||
|
t.Error("expected error message about blockers")
|
||||||
|
}
|
||||||
|
_ = "cannot close " + blockedIssue.ID + ": blocked by open issues" // expectedError
|
||||||
|
if !strings.Contains(closeResp.Error, "blocked by open issues") {
|
||||||
|
t.Errorf("expected error to mention 'blocked by open issues', got: %s", closeResp.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to close with --force flag - should SUCCEED
|
||||||
|
forceCloseArgs := CloseArgs{
|
||||||
|
ID: blockedIssue.ID,
|
||||||
|
Reason: "force closing blocked issue",
|
||||||
|
Force: true,
|
||||||
|
}
|
||||||
|
forceCloseJSON, _ := json.Marshal(forceCloseArgs)
|
||||||
|
forceCloseReq := &Request{
|
||||||
|
Operation: OpClose,
|
||||||
|
Args: forceCloseJSON,
|
||||||
|
Actor: "test-user",
|
||||||
|
}
|
||||||
|
|
||||||
|
forceCloseResp := server.handleClose(forceCloseReq)
|
||||||
|
if !forceCloseResp.Success {
|
||||||
|
t.Errorf("expected force close to succeed, but got error: %s", forceCloseResp.Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHandleClose_BlockerCheck_ClosedBlocker verifies close succeeds when blocker is closed (GH#962)
|
||||||
|
func TestHandleClose_BlockerCheck_ClosedBlocker(t *testing.T) {
|
||||||
|
store := memory.New("/tmp/test.jsonl")
|
||||||
|
server := NewServer("/tmp/test.sock", store, "/tmp", "/tmp/test.db")
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Create two issues
|
||||||
|
blockerArgs := CreateArgs{
|
||||||
|
Title: "Blocker That Will Be Closed",
|
||||||
|
IssueType: "bug",
|
||||||
|
Priority: 1,
|
||||||
|
}
|
||||||
|
blockerJSON, _ := json.Marshal(blockerArgs)
|
||||||
|
blockerReq := &Request{
|
||||||
|
Operation: OpCreate,
|
||||||
|
Args: blockerJSON,
|
||||||
|
Actor: "test-user",
|
||||||
|
}
|
||||||
|
|
||||||
|
blockerResp := server.handleCreate(blockerReq)
|
||||||
|
if !blockerResp.Success {
|
||||||
|
t.Fatalf("failed to create blocker issue: %s", blockerResp.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
var blockerIssue types.Issue
|
||||||
|
if err := json.Unmarshal(blockerResp.Data, &blockerIssue); err != nil {
|
||||||
|
t.Fatalf("failed to parse blocker issue: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
blockedArgs := CreateArgs{
|
||||||
|
Title: "Issue That Can Be Closed After Blocker",
|
||||||
|
IssueType: "task",
|
||||||
|
Priority: 2,
|
||||||
|
}
|
||||||
|
blockedJSON, _ := json.Marshal(blockedArgs)
|
||||||
|
blockedReq := &Request{
|
||||||
|
Operation: OpCreate,
|
||||||
|
Args: blockedJSON,
|
||||||
|
Actor: "test-user",
|
||||||
|
}
|
||||||
|
|
||||||
|
blockedResp := server.handleCreate(blockedReq)
|
||||||
|
if !blockedResp.Success {
|
||||||
|
t.Fatalf("failed to create blocked issue: %s", blockedResp.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
var blockedIssue types.Issue
|
||||||
|
if err := json.Unmarshal(blockedResp.Data, &blockedIssue); err != nil {
|
||||||
|
t.Fatalf("failed to parse blocked issue: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add dependency
|
||||||
|
dep := &types.Dependency{
|
||||||
|
IssueID: blockedIssue.ID,
|
||||||
|
DependsOnID: blockerIssue.ID,
|
||||||
|
Type: types.DepBlocks,
|
||||||
|
}
|
||||||
|
if err := store.AddDependency(ctx, dep, "test-user"); err != nil {
|
||||||
|
t.Fatalf("failed to add dependency: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// First close the blocker
|
||||||
|
closeBlockerArgs := CloseArgs{
|
||||||
|
ID: blockerIssue.ID,
|
||||||
|
Reason: "blocker fixed",
|
||||||
|
}
|
||||||
|
closeBlockerJSON, _ := json.Marshal(closeBlockerArgs)
|
||||||
|
closeBlockerReq := &Request{
|
||||||
|
Operation: OpClose,
|
||||||
|
Args: closeBlockerJSON,
|
||||||
|
Actor: "test-user",
|
||||||
|
}
|
||||||
|
|
||||||
|
closeBlockerResp := server.handleClose(closeBlockerReq)
|
||||||
|
if !closeBlockerResp.Success {
|
||||||
|
t.Fatalf("failed to close blocker: %s", closeBlockerResp.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now close the blocked issue - should SUCCEED because blocker is closed
|
||||||
|
closeBlockedArgs := CloseArgs{
|
||||||
|
ID: blockedIssue.ID,
|
||||||
|
Reason: "now unblocked",
|
||||||
|
}
|
||||||
|
closeBlockedJSON, _ := json.Marshal(closeBlockedArgs)
|
||||||
|
closeBlockedReq := &Request{
|
||||||
|
Operation: OpClose,
|
||||||
|
Args: closeBlockedJSON,
|
||||||
|
Actor: "test-user",
|
||||||
|
}
|
||||||
|
|
||||||
|
closeBlockedResp := server.handleClose(closeBlockedReq)
|
||||||
|
if !closeBlockedResp.Success {
|
||||||
|
t.Errorf("expected close to succeed after blocker was closed, got error: %s", closeBlockedResp.Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user