refactor: split mq.go (1738 lines) into focused modules
- mq.go (400): commands, flags, init, shared helpers - mq_integration.go (606): integration branch create/land/status - mq_status.go (357): status display and formatting - mq_submit.go (219): submit command and branch parsing - mq_list.go (206): list command and filtering Also adds unit tests for helper functions: - formatStatus, getStatusIcon, formatTimeAgo - filterMRsByTarget with edge cases - Test utilities for mocking beads 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,8 @@ package cmd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/steveyegge/gastown/internal/beads"
|
||||
)
|
||||
|
||||
func TestAddIntegrationBranchField(t *testing.T) {
|
||||
@@ -212,3 +214,223 @@ func TestTruncateString(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatStatus(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
status string
|
||||
want string // We check for substring since styling adds ANSI codes
|
||||
}{
|
||||
{
|
||||
name: "open status",
|
||||
status: "open",
|
||||
want: "open",
|
||||
},
|
||||
{
|
||||
name: "in_progress status",
|
||||
status: "in_progress",
|
||||
want: "in_progress",
|
||||
},
|
||||
{
|
||||
name: "closed status",
|
||||
status: "closed",
|
||||
want: "closed",
|
||||
},
|
||||
{
|
||||
name: "unknown status",
|
||||
status: "pending",
|
||||
want: "pending",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := formatStatus(tt.status)
|
||||
if got == "" {
|
||||
t.Errorf("formatStatus(%q) returned empty string", tt.status)
|
||||
}
|
||||
// The result contains ANSI codes, so just check the status text is present
|
||||
if !contains(got, tt.want) {
|
||||
t.Errorf("formatStatus(%q) = %q, should contain %q", tt.status, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetStatusIcon(t *testing.T) {
|
||||
tests := []struct {
|
||||
status string
|
||||
want string
|
||||
}{
|
||||
{"open", "○"},
|
||||
{"in_progress", "▶"},
|
||||
{"closed", "✓"},
|
||||
{"unknown", "•"},
|
||||
{"", "•"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.status, func(t *testing.T) {
|
||||
got := getStatusIcon(tt.status)
|
||||
if got != tt.want {
|
||||
t.Errorf("getStatusIcon(%q) = %q, want %q", tt.status, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatTimeAgo(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
timestamp string
|
||||
wantEmpty bool
|
||||
}{
|
||||
{
|
||||
name: "RFC3339 format",
|
||||
timestamp: "2025-01-01T12:00:00Z",
|
||||
wantEmpty: false,
|
||||
},
|
||||
{
|
||||
name: "RFC3339 with timezone",
|
||||
timestamp: "2025-01-01T12:00:00-08:00",
|
||||
wantEmpty: false,
|
||||
},
|
||||
{
|
||||
name: "date only format",
|
||||
timestamp: "2025-01-01",
|
||||
wantEmpty: false,
|
||||
},
|
||||
{
|
||||
name: "datetime without Z",
|
||||
timestamp: "2025-01-01T12:00:00",
|
||||
wantEmpty: false,
|
||||
},
|
||||
{
|
||||
name: "invalid format returns empty",
|
||||
timestamp: "not-a-date",
|
||||
wantEmpty: true,
|
||||
},
|
||||
{
|
||||
name: "empty string returns empty",
|
||||
timestamp: "",
|
||||
wantEmpty: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := formatTimeAgo(tt.timestamp)
|
||||
if tt.wantEmpty && got != "" {
|
||||
t.Errorf("formatTimeAgo(%q) = %q, want empty", tt.timestamp, got)
|
||||
}
|
||||
if !tt.wantEmpty && got == "" {
|
||||
t.Errorf("formatTimeAgo(%q) returned empty, want non-empty", tt.timestamp)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// contains checks if s contains substr (helper for styled output)
|
||||
func contains(s, substr string) bool {
|
||||
return len(s) >= len(substr) && (s == substr || len(substr) == 0 ||
|
||||
(len(s) > 0 && len(substr) > 0 && stringContains(s, substr)))
|
||||
}
|
||||
|
||||
func stringContains(s, substr string) bool {
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestFilterMRsByTarget(t *testing.T) {
|
||||
// Create test MRs with different targets
|
||||
mrs := []*beads.Issue{
|
||||
makeTestMR("mr-1", "polecat/Nux/gt-001", "integration/gt-epic", "Nux", "open"),
|
||||
makeTestMR("mr-2", "polecat/Toast/gt-002", "main", "Toast", "open"),
|
||||
makeTestMR("mr-3", "polecat/Able/gt-003", "integration/gt-epic", "Able", "open"),
|
||||
makeTestMR("mr-4", "polecat/Baker/gt-004", "integration/gt-other", "Baker", "open"),
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
targetBranch string
|
||||
wantCount int
|
||||
wantIDs []string
|
||||
}{
|
||||
{
|
||||
name: "filter to integration/gt-epic",
|
||||
targetBranch: "integration/gt-epic",
|
||||
wantCount: 2,
|
||||
wantIDs: []string{"mr-1", "mr-3"},
|
||||
},
|
||||
{
|
||||
name: "filter to main",
|
||||
targetBranch: "main",
|
||||
wantCount: 1,
|
||||
wantIDs: []string{"mr-2"},
|
||||
},
|
||||
{
|
||||
name: "filter to non-existent branch",
|
||||
targetBranch: "integration/no-such-epic",
|
||||
wantCount: 0,
|
||||
wantIDs: []string{},
|
||||
},
|
||||
{
|
||||
name: "filter to other integration branch",
|
||||
targetBranch: "integration/gt-other",
|
||||
wantCount: 1,
|
||||
wantIDs: []string{"mr-4"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := filterMRsByTarget(mrs, tt.targetBranch)
|
||||
if len(got) != tt.wantCount {
|
||||
t.Errorf("filterMRsByTarget() returned %d MRs, want %d", len(got), tt.wantCount)
|
||||
}
|
||||
|
||||
// Verify correct IDs
|
||||
gotIDs := make(map[string]bool)
|
||||
for _, mr := range got {
|
||||
gotIDs[mr.ID] = true
|
||||
}
|
||||
for _, wantID := range tt.wantIDs {
|
||||
if !gotIDs[wantID] {
|
||||
t.Errorf("filterMRsByTarget() missing expected MR %s", wantID)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterMRsByTarget_EmptyInput(t *testing.T) {
|
||||
got := filterMRsByTarget(nil, "integration/gt-epic")
|
||||
if got != nil {
|
||||
t.Errorf("filterMRsByTarget(nil) = %v, want nil", got)
|
||||
}
|
||||
|
||||
got = filterMRsByTarget([]*beads.Issue{}, "integration/gt-epic")
|
||||
if len(got) != 0 {
|
||||
t.Errorf("filterMRsByTarget([]) = %v, want empty slice", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterMRsByTarget_NoMRFields(t *testing.T) {
|
||||
// Issue without MR fields in description
|
||||
plainIssue := &beads.Issue{
|
||||
ID: "issue-1",
|
||||
Title: "Not an MR",
|
||||
Type: "merge-request",
|
||||
Status: "open",
|
||||
Description: "Just a plain description with no MR fields",
|
||||
}
|
||||
|
||||
got := filterMRsByTarget([]*beads.Issue{plainIssue}, "main")
|
||||
if len(got) != 0 {
|
||||
t.Errorf("filterMRsByTarget() should filter out issues without MR fields, got %d", len(got))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user