Files
beads/internal/rpc/additional_coverage_test.go
Steve Yegge 05e10b6759 Add comprehensive RPC test coverage (44.7% → 61.7%)
Removed Gate RPC tests that referenced undefined API methods.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-23 23:08:04 -08:00

892 lines
21 KiB
Go

package rpc
import (
"encoding/json"
"os"
"testing"
"time"
"github.com/steveyegge/beads/internal/types"
)
// CountResult represents the response from handleCount
type CountResult struct {
Count int `json:"count"`
}
// TestCount tests the Count operation via RPC
func TestCount(t *testing.T) {
_, client, cleanup := setupTestServer(t)
defer cleanup()
// Create some issues first
for i := 0; i < 5; i++ {
args := &CreateArgs{
Title: "Test Issue for Count",
Description: "Test description",
IssueType: "task",
Priority: 2,
}
if _, err := client.Create(args); err != nil {
t.Fatalf("Create failed: %v", err)
}
}
// Create a closed issue
createResp, err := client.Create(&CreateArgs{
Title: "Closed Issue",
Description: "Test description",
IssueType: "bug",
Priority: 1,
})
if err != nil {
t.Fatalf("Create failed: %v", err)
}
var closedIssue types.Issue
json.Unmarshal(createResp.Data, &closedIssue)
if _, err := client.CloseIssue(&CloseArgs{ID: closedIssue.ID, Reason: "Done"}); err != nil {
t.Fatalf("CloseIssue failed: %v", err)
}
tests := []struct {
name string
args *CountArgs
expectedCount int
}{
{
name: "Count all issues",
args: &CountArgs{},
expectedCount: 6,
},
{
name: "Count open issues",
args: &CountArgs{Status: "open"},
expectedCount: 5,
},
{
name: "Count closed issues",
args: &CountArgs{Status: "closed"},
expectedCount: 1,
},
{
name: "Count by type task",
args: &CountArgs{IssueType: "task"},
expectedCount: 5,
},
{
name: "Count by type bug",
args: &CountArgs{IssueType: "bug"},
expectedCount: 1,
},
{
name: "Count by priority",
args: &CountArgs{Priority: intPtr(2)},
expectedCount: 5,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resp, err := client.Count(tt.args)
if err != nil {
t.Fatalf("Count failed: %v", err)
}
if !resp.Success {
t.Fatalf("Expected success, got error: %s", resp.Error)
}
var result CountResult
if err := json.Unmarshal(resp.Data, &result); err != nil {
t.Fatalf("Failed to unmarshal count result: %v", err)
}
if result.Count != tt.expectedCount {
t.Errorf("Expected count %d, got %d", tt.expectedCount, result.Count)
}
})
}
}
// TestCountWithDateFilters tests Count with date range filters
func TestCountWithDateFilters(t *testing.T) {
_, client, cleanup := setupTestServer(t)
defer cleanup()
// Create an issue
_, err := client.Create(&CreateArgs{
Title: "Recent Issue",
Description: "Test description",
IssueType: "task",
Priority: 2,
})
if err != nil {
t.Fatalf("Create failed: %v", err)
}
// Count with created_after in the past (should include our issue)
yesterday := time.Now().Add(-24 * time.Hour).Format(time.RFC3339)
resp, err := client.Count(&CountArgs{CreatedAfter: yesterday})
if err != nil {
t.Fatalf("Count failed: %v", err)
}
var result CountResult
json.Unmarshal(resp.Data, &result)
if result.Count < 1 {
t.Errorf("Expected at least 1 issue created after %s, got %d", yesterday, result.Count)
}
// Count with created_before in the past (should not include our issue)
resp, err = client.Count(&CountArgs{CreatedBefore: yesterday})
if err != nil {
t.Fatalf("Count failed: %v", err)
}
json.Unmarshal(resp.Data, &result)
if result.Count != 0 {
t.Errorf("Expected 0 issues created before %s, got %d", yesterday, result.Count)
}
}
// TestResolveID tests the ResolveID operation via RPC
func TestResolveID(t *testing.T) {
_, client, cleanup := setupTestServer(t)
defer cleanup()
// Create an issue first
createResp, err := client.Create(&CreateArgs{
Title: "Test Issue for Resolution",
IssueType: "task",
Priority: 2,
})
if err != nil {
t.Fatalf("Create failed: %v", err)
}
var issue types.Issue
json.Unmarshal(createResp.Data, &issue)
// Test resolving the full ID
resp, err := client.ResolveID(&ResolveIDArgs{ID: issue.ID})
if err != nil {
t.Fatalf("ResolveID failed: %v", err)
}
if !resp.Success {
t.Fatalf("Expected success, got error: %s", resp.Error)
}
var resolvedID string
if err := json.Unmarshal(resp.Data, &resolvedID); err != nil {
t.Fatalf("Failed to unmarshal resolved ID: %v", err)
}
if resolvedID != issue.ID {
t.Errorf("Expected resolved ID %s, got %s", issue.ID, resolvedID)
}
// Test resolving a partial ID (first few characters after prefix)
// Note: This depends on there being only one issue with this prefix
if len(issue.ID) > 3 {
partialID := issue.ID[:len(issue.ID)-2] // Remove last 2 chars
resp, err = client.ResolveID(&ResolveIDArgs{ID: partialID})
if err != nil {
t.Fatalf("ResolveID with partial failed: %v", err)
}
if !resp.Success {
// This might fail if partial is ambiguous, which is fine
t.Logf("Partial resolution returned: %s", resp.Error)
}
}
}
// TestResolveID_NotFound tests ResolveID with non-existent ID
func TestResolveID_NotFound(t *testing.T) {
_, client, cleanup := setupTestServer(t)
defer cleanup()
// ResolveID with non-existent ID should fail with an error
resp, err := client.ResolveID(&ResolveIDArgs{ID: "bd-nonexistent"})
// The error is returned through the Execute function
if err != nil {
// Expected - this is the correct behavior for non-existent ID
return
}
// If we got here without error, check the response
if resp.Success {
t.Error("Expected failure for non-existent ID, got success")
}
}
// TestDelete tests the Delete operation via RPC
func TestDelete(t *testing.T) {
_, client, cleanup := setupTestServer(t)
defer cleanup()
// Create an issue to delete
createResp, err := client.Create(&CreateArgs{
Title: "Issue to Delete",
Description: "Test description",
IssueType: "task",
Priority: 2,
})
if err != nil {
t.Fatalf("Create failed: %v", err)
}
var issue types.Issue
json.Unmarshal(createResp.Data, &issue)
// Delete the issue
deleteResp, err := client.Delete(&DeleteArgs{
IDs: []string{issue.ID},
Force: true,
Reason: "Testing deletion",
})
if err != nil {
t.Fatalf("Delete failed: %v", err)
}
if !deleteResp.Success {
t.Fatalf("Expected success, got error: %s", deleteResp.Error)
}
// Verify the delete result
var result map[string]interface{}
json.Unmarshal(deleteResp.Data, &result)
if int(result["deleted_count"].(float64)) != 1 {
t.Errorf("Expected deleted_count=1, got %v", result["deleted_count"])
}
// Note: Deleted issues are tombstoned, not hard-deleted.
// They may still appear with status=closed, so we just verify
// the delete operation succeeded above.
}
// TestDelete_DryRun tests Delete in dry-run mode
func TestDelete_DryRun(t *testing.T) {
_, client, cleanup := setupTestServer(t)
defer cleanup()
// Create an issue
createResp, err := client.Create(&CreateArgs{
Title: "Issue for DryRun Delete",
Description: "Test description",
IssueType: "task",
Priority: 2,
})
if err != nil {
t.Fatalf("Create failed: %v", err)
}
var issue types.Issue
json.Unmarshal(createResp.Data, &issue)
// Delete in dry-run mode
deleteResp, err := client.Delete(&DeleteArgs{
IDs: []string{issue.ID},
DryRun: true,
})
if err != nil {
t.Fatalf("Delete failed: %v", err)
}
if !deleteResp.Success {
t.Fatalf("Expected success, got error: %s", deleteResp.Error)
}
var result map[string]interface{}
json.Unmarshal(deleteResp.Data, &result)
if result["dry_run"] != true {
t.Error("Expected dry_run to be true in response")
}
// Verify issue still exists
showResp, err := client.Show(&ShowArgs{ID: issue.ID})
if err != nil {
t.Fatalf("Show failed: %v", err)
}
if !showResp.Success {
t.Error("Issue should still exist after dry-run delete")
}
}
// TestDelete_NoIDs tests Delete with no IDs
func TestDelete_NoIDs(t *testing.T) {
_, client, cleanup := setupTestServer(t)
defer cleanup()
// Delete with no IDs should fail
resp, err := client.Delete(&DeleteArgs{
IDs: []string{},
})
// The error may come through err or through resp.Success=false
if err != nil {
// Expected - this is the correct behavior
return
}
if resp.Success {
t.Error("Expected failure when deleting with no IDs")
}
}
// TestDelete_MultipleIssues tests deleting multiple issues at once
func TestDelete_MultipleIssues(t *testing.T) {
_, client, cleanup := setupTestServer(t)
defer cleanup()
// Create multiple issues
var ids []string
for i := 0; i < 3; i++ {
createResp, err := client.Create(&CreateArgs{
Title: "Issue for Batch Delete",
Description: "Test description",
IssueType: "task",
Priority: 2,
})
if err != nil {
t.Fatalf("Create failed: %v", err)
}
var issue types.Issue
json.Unmarshal(createResp.Data, &issue)
ids = append(ids, issue.ID)
}
// Delete all at once
deleteResp, err := client.Delete(&DeleteArgs{
IDs: ids,
Force: true,
})
if err != nil {
t.Fatalf("Delete failed: %v", err)
}
if !deleteResp.Success {
t.Fatalf("Expected success, got error: %s", deleteResp.Error)
}
var result map[string]interface{}
json.Unmarshal(deleteResp.Data, &result)
if int(result["deleted_count"].(float64)) != 3 {
t.Errorf("Expected deleted_count=3, got %v", result["deleted_count"])
}
}
// TestStale tests the Stale operation via RPC
func TestStale(t *testing.T) {
_, client, cleanup := setupTestServer(t)
defer cleanup()
// Create an issue (it won't be stale because it's just created)
_, err := client.Create(&CreateArgs{
Title: "Fresh Issue",
Description: "Test description",
IssueType: "task",
Priority: 2,
})
if err != nil {
t.Fatalf("Create failed: %v", err)
}
// Get stale issues (should be empty since issue is fresh)
resp, err := client.Stale(&StaleArgs{Days: 7})
if err != nil {
t.Fatalf("Stale failed: %v", err)
}
if !resp.Success {
t.Fatalf("Expected success, got error: %s", resp.Error)
}
var staleIssues []types.Issue
if err := json.Unmarshal(resp.Data, &staleIssues); err != nil {
t.Fatalf("Failed to unmarshal stale issues: %v", err)
}
// Should be empty since our issue was just created
if len(staleIssues) != 0 {
t.Errorf("Expected 0 stale issues, got %d", len(staleIssues))
}
}
// TestStale_WithStatusFilter tests Stale with status filter
func TestStale_WithStatusFilter(t *testing.T) {
_, client, cleanup := setupTestServer(t)
defer cleanup()
// Create and close an issue
createResp, err := client.Create(&CreateArgs{
Title: "Issue to Close",
Description: "Test description",
IssueType: "task",
Priority: 2,
})
if err != nil {
t.Fatalf("Create failed: %v", err)
}
var issue types.Issue
json.Unmarshal(createResp.Data, &issue)
if _, err := client.CloseIssue(&CloseArgs{ID: issue.ID}); err != nil {
t.Fatalf("CloseIssue failed: %v", err)
}
// Get stale open issues (should not include the closed one)
resp, err := client.Stale(&StaleArgs{Days: 0, Status: "open"})
if err != nil {
t.Fatalf("Stale failed: %v", err)
}
if !resp.Success {
t.Fatalf("Expected success, got error: %s", resp.Error)
}
var staleIssues []types.Issue
json.Unmarshal(resp.Data, &staleIssues)
for _, si := range staleIssues {
if si.ID == issue.ID {
t.Error("Closed issue should not appear in open stale issues")
}
}
}
// TestCommentList tests the ListComments operation via RPC
func TestCommentList(t *testing.T) {
_, client, cleanup := setupTestServer(t)
defer cleanup()
// Create an issue first
createResp, err := client.Create(&CreateArgs{
Title: "Issue for Comments",
Description: "Test description",
IssueType: "task",
Priority: 2,
})
if err != nil {
t.Fatalf("Create failed: %v", err)
}
var issue types.Issue
json.Unmarshal(createResp.Data, &issue)
// List comments on the issue (should be empty initially)
resp, err := client.ListComments(&CommentListArgs{ID: issue.ID})
if err != nil {
t.Fatalf("ListComments failed: %v", err)
}
if !resp.Success {
t.Fatalf("Expected success, got error: %s", resp.Error)
}
var comments []types.Comment
if err := json.Unmarshal(resp.Data, &comments); err != nil {
t.Fatalf("Failed to unmarshal comments: %v", err)
}
if len(comments) != 0 {
t.Errorf("Expected 0 comments initially, got %d", len(comments))
}
}
// TestCommentAdd tests the AddComment operation via RPC
func TestCommentAdd(t *testing.T) {
_, client, cleanup := setupTestServer(t)
defer cleanup()
// Create an issue first
createResp, err := client.Create(&CreateArgs{
Title: "Issue for Adding Comments",
Description: "Test description",
IssueType: "task",
Priority: 2,
})
if err != nil {
t.Fatalf("Create failed: %v", err)
}
var issue types.Issue
json.Unmarshal(createResp.Data, &issue)
// Add a comment
addResp, err := client.AddComment(&CommentAddArgs{
ID: issue.ID,
Author: "testuser",
Text: "This is a test comment",
})
if err != nil {
t.Fatalf("AddComment failed: %v", err)
}
if !addResp.Success {
t.Fatalf("Expected success, got error: %s", addResp.Error)
}
var comment types.Comment
if err := json.Unmarshal(addResp.Data, &comment); err != nil {
t.Fatalf("Failed to unmarshal comment: %v", err)
}
if comment.Author != "testuser" {
t.Errorf("Expected author 'testuser', got '%s'", comment.Author)
}
if comment.Text != "This is a test comment" {
t.Errorf("Expected text 'This is a test comment', got '%s'", comment.Text)
}
// Verify comment is listed
listResp, err := client.ListComments(&CommentListArgs{ID: issue.ID})
if err != nil {
t.Fatalf("ListComments failed: %v", err)
}
var comments []types.Comment
json.Unmarshal(listResp.Data, &comments)
if len(comments) != 1 {
t.Errorf("Expected 1 comment, got %d", len(comments))
}
}
// TestCommentAdd_MultipleComments tests adding multiple comments
func TestCommentAdd_MultipleComments(t *testing.T) {
_, client, cleanup := setupTestServer(t)
defer cleanup()
// Create an issue
createResp, err := client.Create(&CreateArgs{
Title: "Issue for Multiple Comments",
Description: "Test description",
IssueType: "task",
Priority: 2,
})
if err != nil {
t.Fatalf("Create failed: %v", err)
}
var issue types.Issue
json.Unmarshal(createResp.Data, &issue)
// Add multiple comments
for i := 1; i <= 3; i++ {
_, err := client.AddComment(&CommentAddArgs{
ID: issue.ID,
Author: "user",
Text: "Comment text",
})
if err != nil {
t.Fatalf("AddComment %d failed: %v", i, err)
}
}
// Verify all comments are listed
listResp, err := client.ListComments(&CommentListArgs{ID: issue.ID})
if err != nil {
t.Fatalf("ListComments failed: %v", err)
}
var comments []types.Comment
json.Unmarshal(listResp.Data, &comments)
if len(comments) != 3 {
t.Errorf("Expected 3 comments, got %d", len(comments))
}
}
// TestMetrics tests the Metrics operation via RPC
func TestMetrics(t *testing.T) {
_, client, cleanup := setupTestServer(t)
defer cleanup()
// Make a few requests to generate some metrics
for i := 0; i < 3; i++ {
if err := client.Ping(); err != nil {
t.Fatalf("Ping failed: %v", err)
}
}
// Get metrics
metrics, err := client.Metrics()
if err != nil {
t.Fatalf("Metrics failed: %v", err)
}
if metrics == nil {
t.Fatal("Expected metrics, got nil")
}
// Check that we have operations recorded
if len(metrics.Operations) == 0 {
t.Error("Expected at least some operations recorded in metrics")
}
// Calculate total requests from operations
var totalRequests int64
for _, op := range metrics.Operations {
totalRequests += op.TotalCount
}
// We made 3 pings + 1 metrics request = at least 4 requests
// (metrics call itself is recorded after snapshot)
if totalRequests < 3 {
t.Errorf("Expected at least 3 total requests, got %d", totalRequests)
}
// Check uptime is reasonable
if metrics.UptimeSeconds <= 0 {
t.Errorf("Expected positive uptime, got %f", metrics.UptimeSeconds)
}
}
// TestCountWithGroupBy tests Count with GroupBy option
func TestCountWithGroupBy(t *testing.T) {
_, client, cleanup := setupTestServer(t)
defer cleanup()
// Create issues with different statuses and types
_, err := client.Create(&CreateArgs{
Title: "Task Issue",
Description: "Test description",
IssueType: "task",
Priority: 2,
})
if err != nil {
t.Fatalf("Create failed: %v", err)
}
createResp, err := client.Create(&CreateArgs{
Title: "Bug Issue",
Description: "Test description",
IssueType: "bug",
Priority: 1,
})
if err != nil {
t.Fatalf("Create failed: %v", err)
}
var bugIssue types.Issue
json.Unmarshal(createResp.Data, &bugIssue)
// Close the bug
if _, err := client.CloseIssue(&CloseArgs{ID: bugIssue.ID}); err != nil {
t.Fatalf("CloseIssue failed: %v", err)
}
// Count grouped by status
resp, err := client.Count(&CountArgs{GroupBy: "status"})
if err != nil {
t.Fatalf("Count with GroupBy failed: %v", err)
}
if !resp.Success {
t.Fatalf("Expected success, got error: %s", resp.Error)
}
// The response should contain grouped data
// (The exact format depends on the server implementation)
if len(resp.Data) == 0 {
t.Error("Expected non-empty response for grouped count")
}
}
// TestCountWithTitleContains tests Count with title pattern matching
func TestCountWithTitleContains(t *testing.T) {
_, client, cleanup := setupTestServer(t)
defer cleanup()
// Create issues with specific titles
_, err := client.Create(&CreateArgs{
Title: "Authentication Bug Fix",
Description: "Test description",
IssueType: "bug",
Priority: 1,
})
if err != nil {
t.Fatalf("Create failed: %v", err)
}
_, err = client.Create(&CreateArgs{
Title: "Add User Login Feature",
Description: "Test description",
IssueType: "feature",
Priority: 2,
})
if err != nil {
t.Fatalf("Create failed: %v", err)
}
// Count issues with "Authentication" in title
resp, err := client.Count(&CountArgs{TitleContains: "Authentication"})
if err != nil {
t.Fatalf("Count failed: %v", err)
}
var result CountResult
json.Unmarshal(resp.Data, &result)
if result.Count != 1 {
t.Errorf("Expected 1 issue with 'Authentication' in title, got %d", result.Count)
}
}
// TestStaleWithLimit tests Stale with limit parameter
func TestStaleWithLimit(t *testing.T) {
_, client, cleanup := setupTestServer(t)
defer cleanup()
// Create multiple issues
for i := 0; i < 5; i++ {
_, err := client.Create(&CreateArgs{
Title: "Issue for Stale Test",
Description: "Test description",
IssueType: "task",
Priority: 2,
})
if err != nil {
t.Fatalf("Create failed: %v", err)
}
}
// Get stale issues with limit (using 0 days so all issues are considered stale)
resp, err := client.Stale(&StaleArgs{Days: 0, Limit: 2})
if err != nil {
t.Fatalf("Stale failed: %v", err)
}
if !resp.Success {
t.Fatalf("Expected success, got error: %s", resp.Error)
}
var staleIssues []types.Issue
json.Unmarshal(resp.Data, &staleIssues)
if len(staleIssues) > 2 {
t.Errorf("Expected at most 2 stale issues (limit), got %d", len(staleIssues))
}
}
// Helper function to create a pointer to an int
func intPtr(i int) *int {
return &i
}
// GetMutations and Export tests
// TestGetMutations tests the GetMutations operation via RPC
func TestGetMutations(t *testing.T) {
_, client, cleanup := setupTestServer(t)
defer cleanup()
// Create an issue to generate a mutation
_, err := client.Create(&CreateArgs{
Title: "Issue to track mutations",
Description: "Test description",
IssueType: "task",
Priority: 2,
})
if err != nil {
t.Fatalf("Create failed: %v", err)
}
// Get recent mutations
resp, err := client.GetMutations(&GetMutationsArgs{Since: 0})
if err != nil {
t.Fatalf("GetMutations failed: %v", err)
}
if !resp.Success {
t.Fatalf("Expected success, got error: %s", resp.Error)
}
// Response should be a slice of mutations
if len(resp.Data) == 0 {
t.Error("Expected non-empty response with mutations")
}
}
// TestExport tests the Export operation via RPC
func TestExport(t *testing.T) {
_, client, cleanup := setupTestServer(t)
defer cleanup()
// Create some issues first
for i := 0; i < 3; i++ {
_, err := client.Create(&CreateArgs{
Title: "Issue for Export",
Description: "Test description",
IssueType: "task",
Priority: 2,
})
if err != nil {
t.Fatalf("Create failed: %v", err)
}
}
// Create a temp file for export
tmpFile, err := os.CreateTemp("", "beads-export-*.jsonl")
if err != nil {
t.Fatalf("Failed to create temp file: %v", err)
}
defer os.Remove(tmpFile.Name())
tmpFile.Close()
// Export to the temp file
resp, err := client.Export(&ExportArgs{
JSONLPath: tmpFile.Name(),
})
if err != nil {
t.Fatalf("Export failed: %v", err)
}
if !resp.Success {
t.Fatalf("Expected success, got error: %s", resp.Error)
}
// Verify file was written
info, err := os.Stat(tmpFile.Name())
if err != nil {
t.Fatalf("Failed to stat export file: %v", err)
}
if info.Size() == 0 {
t.Error("Expected non-empty export file")
}
}
// TestMutationChan tests access to the mutation channel
func TestMutationChan(t *testing.T) {
server, _, cleanup := setupTestServer(t)
defer cleanup()
// Get the mutation channel
ch := server.MutationChan()
// Channel should be non-nil
if ch == nil {
t.Error("Expected non-nil mutation channel")
}
}
// TestResetDroppedEventsCount tests resetting the dropped events counter
func TestResetDroppedEventsCount(t *testing.T) {
server, _, cleanup := setupTestServer(t)
defer cleanup()
// Reset dropped events count (should not panic)
server.ResetDroppedEventsCount()
// No error means success
}