Files
gastown/internal/beads/beads_test.go
Steve Yegge 4048cdc373 fix(lint): resolve all errcheck warnings
Fix ~50 errcheck warnings across the codebase:

- Add explicit `_ =` for intentionally ignored error returns (cleanup,
  best-effort operations, etc.)
- Use `defer func() { _ = ... }()` pattern for defer statements
- Handle tmux SetEnvironment, KillSession, SendKeysRaw returns
- Handle mail router.Send returns
- Handle os.RemoveAll, os.Rename in cleanup paths
- Handle rand.Read returns for ID generation
- Handle fmt.Fprint* returns when writing to io.Writer
- Fix for-select with single case to use for-range
- Handle cobra MarkFlagRequired returns

All tests pass. Code compiles without errors.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-19 12:44:42 -08:00

629 lines
14 KiB
Go

package beads
import (
"os"
"path/filepath"
"strings"
"testing"
)
// TestNew verifies the constructor.
func TestNew(t *testing.T) {
b := New("/some/path")
if b == nil {
t.Fatal("New returned nil")
}
if b.workDir != "/some/path" {
t.Errorf("workDir = %q, want /some/path", b.workDir)
}
}
// TestListOptions verifies ListOptions defaults.
func TestListOptions(t *testing.T) {
opts := ListOptions{
Status: "open",
Type: "task",
Priority: 1,
}
if opts.Status != "open" {
t.Errorf("Status = %q, want open", opts.Status)
}
}
// TestCreateOptions verifies CreateOptions fields.
func TestCreateOptions(t *testing.T) {
opts := CreateOptions{
Title: "Test issue",
Type: "task",
Priority: 2,
Description: "A test description",
Parent: "gt-abc",
}
if opts.Title != "Test issue" {
t.Errorf("Title = %q, want 'Test issue'", opts.Title)
}
if opts.Parent != "gt-abc" {
t.Errorf("Parent = %q, want gt-abc", opts.Parent)
}
}
// TestUpdateOptions verifies UpdateOptions pointer fields.
func TestUpdateOptions(t *testing.T) {
status := "in_progress"
priority := 1
opts := UpdateOptions{
Status: &status,
Priority: &priority,
}
if *opts.Status != "in_progress" {
t.Errorf("Status = %q, want in_progress", *opts.Status)
}
if *opts.Priority != 1 {
t.Errorf("Priority = %d, want 1", *opts.Priority)
}
}
// TestIsBeadsRepo tests repository detection.
func TestIsBeadsRepo(t *testing.T) {
// Test with a non-beads directory
tmpDir, err := os.MkdirTemp("", "beads-test-*")
if err != nil {
t.Fatal(err)
}
defer func() { _ = os.RemoveAll(tmpDir) }()
b := New(tmpDir)
// This should return false since there's no .beads directory
// and bd list will fail
if b.IsBeadsRepo() {
// This might pass if bd handles missing .beads gracefully
t.Log("IsBeadsRepo returned true for non-beads directory (bd might initialize)")
}
}
// TestWrapError tests error wrapping.
func TestWrapError(t *testing.T) {
b := New("/test")
tests := []struct {
stderr string
wantErr error
wantNil bool
}{
{"not a beads repository", ErrNotARepo, false},
{"No .beads directory found", ErrNotARepo, false},
{".beads directory not found", ErrNotARepo, false},
{"sync conflict detected", ErrSyncConflict, false},
{"CONFLICT in file.md", ErrSyncConflict, false},
{"Issue not found: gt-xyz", ErrNotFound, false},
{"gt-xyz not found", ErrNotFound, false},
}
for _, tt := range tests {
err := b.wrapError(nil, tt.stderr, []string{"test"})
if tt.wantNil {
if err != nil {
t.Errorf("wrapError(%q) = %v, want nil", tt.stderr, err)
}
} else {
if err != tt.wantErr {
t.Errorf("wrapError(%q) = %v, want %v", tt.stderr, err, tt.wantErr)
}
}
}
}
// Integration test that runs against real bd if available
func TestIntegration(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
// Find a beads repo (use current directory if it has .beads)
cwd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
// Walk up to find .beads
dir := cwd
for {
if _, err := os.Stat(filepath.Join(dir, ".beads")); err == nil {
break
}
parent := filepath.Dir(dir)
if parent == dir {
t.Skip("no .beads directory found in path")
}
dir = parent
}
b := New(dir)
// Test List
t.Run("List", func(t *testing.T) {
issues, err := b.List(ListOptions{Status: "open"})
if err != nil {
t.Fatalf("List failed: %v", err)
}
t.Logf("Found %d open issues", len(issues))
})
// Test Ready
t.Run("Ready", func(t *testing.T) {
issues, err := b.Ready()
if err != nil {
t.Fatalf("Ready failed: %v", err)
}
t.Logf("Found %d ready issues", len(issues))
})
// Test Blocked
t.Run("Blocked", func(t *testing.T) {
issues, err := b.Blocked()
if err != nil {
t.Fatalf("Blocked failed: %v", err)
}
t.Logf("Found %d blocked issues", len(issues))
})
// Test Show (if we have issues)
t.Run("Show", func(t *testing.T) {
issues, err := b.List(ListOptions{})
if err != nil {
t.Fatalf("List failed: %v", err)
}
if len(issues) == 0 {
t.Skip("no issues to show")
}
issue, err := b.Show(issues[0].ID)
if err != nil {
t.Fatalf("Show(%s) failed: %v", issues[0].ID, err)
}
t.Logf("Showed issue: %s - %s", issue.ID, issue.Title)
})
}
// TestParseMRFields tests parsing MR fields from issue descriptions.
func TestParseMRFields(t *testing.T) {
tests := []struct {
name string
issue *Issue
wantNil bool
wantFields *MRFields
}{
{
name: "nil issue",
issue: nil,
wantNil: true,
},
{
name: "empty description",
issue: &Issue{Description: ""},
wantNil: true,
},
{
name: "no MR fields",
issue: &Issue{Description: "This is just plain text\nwith no field markers"},
wantNil: true,
},
{
name: "all fields",
issue: &Issue{
Description: `branch: polecat/Nux/gt-xyz
target: main
source_issue: gt-xyz
worker: Nux
rig: gastown
merge_commit: abc123def
close_reason: merged`,
},
wantFields: &MRFields{
Branch: "polecat/Nux/gt-xyz",
Target: "main",
SourceIssue: "gt-xyz",
Worker: "Nux",
Rig: "gastown",
MergeCommit: "abc123def",
CloseReason: "merged",
},
},
{
name: "partial fields",
issue: &Issue{
Description: `branch: polecat/Toast/gt-abc
target: integration/gt-epic
source_issue: gt-abc
worker: Toast`,
},
wantFields: &MRFields{
Branch: "polecat/Toast/gt-abc",
Target: "integration/gt-epic",
SourceIssue: "gt-abc",
Worker: "Toast",
},
},
{
name: "mixed with prose",
issue: &Issue{
Description: `branch: polecat/Capable/gt-def
target: main
source_issue: gt-def
This MR fixes a critical bug in the authentication system.
Please review carefully.
worker: Capable
rig: wasteland`,
},
wantFields: &MRFields{
Branch: "polecat/Capable/gt-def",
Target: "main",
SourceIssue: "gt-def",
Worker: "Capable",
Rig: "wasteland",
},
},
{
name: "alternate key formats",
issue: &Issue{
Description: `branch: polecat/Max/gt-ghi
source-issue: gt-ghi
merge-commit: 789xyz`,
},
wantFields: &MRFields{
Branch: "polecat/Max/gt-ghi",
SourceIssue: "gt-ghi",
MergeCommit: "789xyz",
},
},
{
name: "case insensitive keys",
issue: &Issue{
Description: `Branch: polecat/Furiosa/gt-jkl
TARGET: main
Worker: Furiosa
RIG: gastown`,
},
wantFields: &MRFields{
Branch: "polecat/Furiosa/gt-jkl",
Target: "main",
Worker: "Furiosa",
Rig: "gastown",
},
},
{
name: "extra whitespace",
issue: &Issue{
Description: ` branch: polecat/Nux/gt-mno
target:main
worker: Nux `,
},
wantFields: &MRFields{
Branch: "polecat/Nux/gt-mno",
Target: "main",
Worker: "Nux",
},
},
{
name: "ignores empty values",
issue: &Issue{
Description: `branch: polecat/Nux/gt-pqr
target:
source_issue: gt-pqr`,
},
wantFields: &MRFields{
Branch: "polecat/Nux/gt-pqr",
SourceIssue: "gt-pqr",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fields := ParseMRFields(tt.issue)
if tt.wantNil {
if fields != nil {
t.Errorf("ParseMRFields() = %+v, want nil", fields)
}
return
}
if fields == nil {
t.Fatal("ParseMRFields() = nil, want non-nil")
}
if fields.Branch != tt.wantFields.Branch {
t.Errorf("Branch = %q, want %q", fields.Branch, tt.wantFields.Branch)
}
if fields.Target != tt.wantFields.Target {
t.Errorf("Target = %q, want %q", fields.Target, tt.wantFields.Target)
}
if fields.SourceIssue != tt.wantFields.SourceIssue {
t.Errorf("SourceIssue = %q, want %q", fields.SourceIssue, tt.wantFields.SourceIssue)
}
if fields.Worker != tt.wantFields.Worker {
t.Errorf("Worker = %q, want %q", fields.Worker, tt.wantFields.Worker)
}
if fields.Rig != tt.wantFields.Rig {
t.Errorf("Rig = %q, want %q", fields.Rig, tt.wantFields.Rig)
}
if fields.MergeCommit != tt.wantFields.MergeCommit {
t.Errorf("MergeCommit = %q, want %q", fields.MergeCommit, tt.wantFields.MergeCommit)
}
if fields.CloseReason != tt.wantFields.CloseReason {
t.Errorf("CloseReason = %q, want %q", fields.CloseReason, tt.wantFields.CloseReason)
}
})
}
}
// TestFormatMRFields tests formatting MR fields to string.
func TestFormatMRFields(t *testing.T) {
tests := []struct {
name string
fields *MRFields
want string
}{
{
name: "nil fields",
fields: nil,
want: "",
},
{
name: "empty fields",
fields: &MRFields{},
want: "",
},
{
name: "all fields",
fields: &MRFields{
Branch: "polecat/Nux/gt-xyz",
Target: "main",
SourceIssue: "gt-xyz",
Worker: "Nux",
Rig: "gastown",
MergeCommit: "abc123def",
CloseReason: "merged",
},
want: `branch: polecat/Nux/gt-xyz
target: main
source_issue: gt-xyz
worker: Nux
rig: gastown
merge_commit: abc123def
close_reason: merged`,
},
{
name: "partial fields",
fields: &MRFields{
Branch: "polecat/Toast/gt-abc",
Target: "main",
SourceIssue: "gt-abc",
Worker: "Toast",
},
want: `branch: polecat/Toast/gt-abc
target: main
source_issue: gt-abc
worker: Toast`,
},
{
name: "only close fields",
fields: &MRFields{
MergeCommit: "deadbeef",
CloseReason: "rejected",
},
want: `merge_commit: deadbeef
close_reason: rejected`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := FormatMRFields(tt.fields)
if got != tt.want {
t.Errorf("FormatMRFields() =\n%q\nwant\n%q", got, tt.want)
}
})
}
}
// TestSetMRFields tests updating issue descriptions with MR fields.
func TestSetMRFields(t *testing.T) {
tests := []struct {
name string
issue *Issue
fields *MRFields
want string
}{
{
name: "nil issue",
issue: nil,
fields: &MRFields{
Branch: "polecat/Nux/gt-xyz",
Target: "main",
},
want: `branch: polecat/Nux/gt-xyz
target: main`,
},
{
name: "empty description",
issue: &Issue{Description: ""},
fields: &MRFields{
Branch: "polecat/Nux/gt-xyz",
Target: "main",
SourceIssue: "gt-xyz",
},
want: `branch: polecat/Nux/gt-xyz
target: main
source_issue: gt-xyz`,
},
{
name: "preserve prose content",
issue: &Issue{Description: "This is a description of the work.\n\nIt spans multiple lines."},
fields: &MRFields{
Branch: "polecat/Toast/gt-abc",
Worker: "Toast",
},
want: `branch: polecat/Toast/gt-abc
worker: Toast
This is a description of the work.
It spans multiple lines.`,
},
{
name: "replace existing fields",
issue: &Issue{
Description: `branch: polecat/Nux/gt-old
target: develop
source_issue: gt-old
worker: Nux
Some existing prose content.`,
},
fields: &MRFields{
Branch: "polecat/Nux/gt-new",
Target: "main",
SourceIssue: "gt-new",
Worker: "Nux",
MergeCommit: "abc123",
},
want: `branch: polecat/Nux/gt-new
target: main
source_issue: gt-new
worker: Nux
merge_commit: abc123
Some existing prose content.`,
},
{
name: "preserve non-MR key-value lines",
issue: &Issue{
Description: `branch: polecat/Capable/gt-def
custom_field: some value
author: someone
target: main`,
},
fields: &MRFields{
Branch: "polecat/Capable/gt-ghi",
Target: "integration/epic",
CloseReason: "merged",
},
want: `branch: polecat/Capable/gt-ghi
target: integration/epic
close_reason: merged
custom_field: some value
author: someone`,
},
{
name: "empty fields clears MR data",
issue: &Issue{Description: "branch: old\ntarget: old\n\nKeep this text."},
fields: &MRFields{},
want: "Keep this text.",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := SetMRFields(tt.issue, tt.fields)
if got != tt.want {
t.Errorf("SetMRFields() =\n%q\nwant\n%q", got, tt.want)
}
})
}
}
// TestMRFieldsRoundTrip tests that parse/format round-trips correctly.
func TestMRFieldsRoundTrip(t *testing.T) {
original := &MRFields{
Branch: "polecat/Nux/gt-xyz",
Target: "main",
SourceIssue: "gt-xyz",
Worker: "Nux",
Rig: "gastown",
MergeCommit: "abc123def789",
CloseReason: "merged",
}
// Format to string
formatted := FormatMRFields(original)
// Parse back
issue := &Issue{Description: formatted}
parsed := ParseMRFields(issue)
if parsed == nil {
t.Fatal("round-trip parse returned nil")
}
if *parsed != *original {
t.Errorf("round-trip mismatch:\ngot %+v\nwant %+v", parsed, original)
}
}
// TestParseMRFieldsFromDesignDoc tests the example from the design doc.
func TestParseMRFieldsFromDesignDoc(t *testing.T) {
// Example from docs/merge-queue-design.md
description := `branch: polecat/Nux/gt-xyz
target: main
source_issue: gt-xyz
worker: Nux
rig: gastown`
issue := &Issue{Description: description}
fields := ParseMRFields(issue)
if fields == nil {
t.Fatal("ParseMRFields returned nil for design doc example")
}
// Verify all fields match the design doc
if fields.Branch != "polecat/Nux/gt-xyz" {
t.Errorf("Branch = %q, want polecat/Nux/gt-xyz", fields.Branch)
}
if fields.Target != "main" {
t.Errorf("Target = %q, want main", fields.Target)
}
if fields.SourceIssue != "gt-xyz" {
t.Errorf("SourceIssue = %q, want gt-xyz", fields.SourceIssue)
}
if fields.Worker != "Nux" {
t.Errorf("Worker = %q, want Nux", fields.Worker)
}
if fields.Rig != "gastown" {
t.Errorf("Rig = %q, want gastown", fields.Rig)
}
}
// TestSetMRFieldsPreservesURL tests that URLs in prose are preserved.
func TestSetMRFieldsPreservesURL(t *testing.T) {
// URLs contain colons which could be confused with key: value
issue := &Issue{
Description: `branch: old-branch
Check out https://example.com/path for more info.
Also see http://localhost:8080/api`,
}
fields := &MRFields{
Branch: "new-branch",
Target: "main",
}
result := SetMRFields(issue, fields)
// URLs should be preserved
if !strings.Contains(result, "https://example.com/path") {
t.Error("HTTPS URL was not preserved")
}
if !strings.Contains(result, "http://localhost:8080/api") {
t.Error("HTTP URL was not preserved")
}
if !strings.Contains(result, "branch: new-branch") {
t.Error("branch field was not set")
}
}