package importer import ( "testing" "time" "github.com/steveyegge/beads/internal/types" ) func TestIssueDataChanged(t *testing.T) { baseIssue := &types.Issue{ ID: "test-1", Title: "Original Title", Description: "Original Description", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask, Design: "Design notes", AcceptanceCriteria: "Acceptance", Notes: "Notes", Assignee: "john", } tests := []struct { name string updates map[string]interface{} expected bool }{ { name: "no changes", updates: map[string]interface{}{ "title": "Original Title", }, expected: false, }, { name: "title changed", updates: map[string]interface{}{ "title": "New Title", }, expected: true, }, { name: "description changed", updates: map[string]interface{}{ "description": "New Description", }, expected: true, }, { name: "status changed", updates: map[string]interface{}{ "status": types.StatusClosed, }, expected: true, }, { name: "status string changed", updates: map[string]interface{}{ "status": "closed", }, expected: true, }, { name: "priority changed", updates: map[string]interface{}{ "priority": 2, }, expected: true, }, { name: "priority float64 changed", updates: map[string]interface{}{ "priority": float64(2), }, expected: true, }, { name: "issue_type changed", updates: map[string]interface{}{ "issue_type": types.TypeBug, }, expected: true, }, { name: "design changed", updates: map[string]interface{}{ "design": "New design", }, expected: true, }, { name: "acceptance_criteria changed", updates: map[string]interface{}{ "acceptance_criteria": "New acceptance", }, expected: true, }, { name: "notes changed", updates: map[string]interface{}{ "notes": "New notes", }, expected: true, }, { name: "assignee changed", updates: map[string]interface{}{ "assignee": "jane", }, expected: true, }, { name: "multiple fields same", updates: map[string]interface{}{ "title": "Original Title", "priority": 1, "status": types.StatusOpen, }, expected: false, }, { name: "one field changed in multiple", updates: map[string]interface{}{ "title": "Original Title", "priority": 2, // Changed "status": types.StatusOpen, }, expected: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := IssueDataChanged(baseIssue, tt.updates) if result != tt.expected { t.Errorf("Expected %v, got %v", tt.expected, result) } }) } } func TestFieldComparator_StringConversion(t *testing.T) { fc := newFieldComparator() tests := []struct { name string value interface{} wantStr string wantOk bool }{ {"string", "hello", "hello", true}, {"string pointer", stringPtr("world"), "world", true}, {"nil string pointer", (*string)(nil), "", true}, {"nil", nil, "", true}, {"int (invalid)", 123, "", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { str, ok := fc.strFrom(tt.value) if ok != tt.wantOk { t.Errorf("Expected ok=%v, got ok=%v", tt.wantOk, ok) } if ok && str != tt.wantStr { t.Errorf("Expected str=%q, got %q", tt.wantStr, str) } }) } } func TestFieldComparator_IntConversion(t *testing.T) { fc := newFieldComparator() tests := []struct { name string value interface{} wantInt int64 wantOk bool }{ {"int", 42, 42, true}, {"int32", int32(42), 42, true}, {"int64", int64(42), 42, true}, {"float64 integer", float64(42), 42, true}, {"float64 fractional", 42.5, 0, false}, {"string (invalid)", "123", 0, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { i, ok := fc.intFrom(tt.value) if ok != tt.wantOk { t.Errorf("Expected ok=%v, got ok=%v", tt.wantOk, ok) } if ok && i != tt.wantInt { t.Errorf("Expected int=%d, got %d", tt.wantInt, i) } }) } } func TestRenameImportedIssuePrefixes(t *testing.T) { t.Run("rename single issue", func(t *testing.T) { issues := []*types.Issue{ { ID: "old-1", Title: "Test Issue", }, } err := RenameImportedIssuePrefixes(issues, "new") if err != nil { t.Fatalf("Expected no error, got: %v", err) } if issues[0].ID != "new-1" { t.Errorf("Expected ID 'new-1', got '%s'", issues[0].ID) } }) t.Run("rename multiple issues", func(t *testing.T) { issues := []*types.Issue{ {ID: "old-1", Title: "Issue 1"}, {ID: "old-2", Title: "Issue 2"}, {ID: "other-3", Title: "Issue 3"}, } err := RenameImportedIssuePrefixes(issues, "new") if err != nil { t.Fatalf("Expected no error, got: %v", err) } if issues[0].ID != "new-1" { t.Errorf("Expected ID 'new-1', got '%s'", issues[0].ID) } if issues[1].ID != "new-2" { t.Errorf("Expected ID 'new-2', got '%s'", issues[1].ID) } if issues[2].ID != "new-3" { t.Errorf("Expected ID 'new-3', got '%s'", issues[2].ID) } }) t.Run("rename with dependencies", func(t *testing.T) { issues := []*types.Issue{ { ID: "old-1", Title: "Issue 1", Dependencies: []*types.Dependency{ {IssueID: "old-1", DependsOnID: "old-2", Type: types.DepBlocks}, }, }, { ID: "old-2", Title: "Issue 2", }, } err := RenameImportedIssuePrefixes(issues, "new") if err != nil { t.Fatalf("Expected no error, got: %v", err) } if issues[0].Dependencies[0].IssueID != "new-1" { t.Errorf("Expected dependency IssueID 'new-1', got '%s'", issues[0].Dependencies[0].IssueID) } if issues[0].Dependencies[0].DependsOnID != "new-2" { t.Errorf("Expected dependency DependsOnID 'new-2', got '%s'", issues[0].Dependencies[0].DependsOnID) } }) t.Run("rename with text references", func(t *testing.T) { issues := []*types.Issue{ { ID: "old-1", Title: "Refers to old-2", Description: "See old-2 for details", Design: "Depends on old-2", AcceptanceCriteria: "After old-2 is done", Notes: "Related: old-2", }, { ID: "old-2", Title: "Issue 2", }, } err := RenameImportedIssuePrefixes(issues, "new") if err != nil { t.Fatalf("Expected no error, got: %v", err) } if issues[0].Title != "Refers to new-2" { t.Errorf("Expected title with new-2, got '%s'", issues[0].Title) } if issues[0].Description != "See new-2 for details" { t.Errorf("Expected description with new-2, got '%s'", issues[0].Description) } }) t.Run("rename with comments", func(t *testing.T) { issues := []*types.Issue{ { ID: "old-1", Title: "Issue 1", Comments: []*types.Comment{ { ID: 0, IssueID: "old-1", Author: "test", Text: "Related to old-2", CreatedAt: time.Now(), }, }, }, { ID: "old-2", Title: "Issue 2", }, } err := RenameImportedIssuePrefixes(issues, "new") if err != nil { t.Fatalf("Expected no error, got: %v", err) } if issues[0].Comments[0].Text != "Related to new-2" { t.Errorf("Expected comment with new-2, got '%s'", issues[0].Comments[0].Text) } }) t.Run("error on malformed ID", func(t *testing.T) { issues := []*types.Issue{ {ID: "nohyphen", Title: "Invalid"}, } err := RenameImportedIssuePrefixes(issues, "new") if err == nil { t.Error("Expected error for malformed ID") } }) t.Run("error on non-numeric suffix", func(t *testing.T) { issues := []*types.Issue{ {ID: "old-abc", Title: "Invalid"}, } err := RenameImportedIssuePrefixes(issues, "new") if err == nil { t.Error("Expected error for non-numeric suffix") } }) t.Run("no rename when prefix matches", func(t *testing.T) { issues := []*types.Issue{ {ID: "same-1", Title: "Issue 1"}, } err := RenameImportedIssuePrefixes(issues, "same") if err != nil { t.Fatalf("Expected no error, got: %v", err) } if issues[0].ID != "same-1" { t.Errorf("Expected ID unchanged 'same-1', got '%s'", issues[0].ID) } }) } func TestReplaceBoundaryAware(t *testing.T) { tests := []struct { name string text string oldID string newID string want string }{ { name: "simple replacement", text: "See old-1 for details", oldID: "old-1", newID: "new-1", want: "See new-1 for details", }, { name: "multiple occurrences", text: "old-1 and old-1 again", oldID: "old-1", newID: "new-1", want: "new-1 and new-1 again", }, { name: "no match substring prefix", text: "old-10 should not match", oldID: "old-1", newID: "new-1", want: "old-10 should not match", }, { name: "match at end of longer ID", text: "should not match old-1 at end", oldID: "old-1", newID: "new-1", want: "should not match new-1 at end", }, { name: "boundary at start", text: "old-1 starts here", oldID: "old-1", newID: "new-1", want: "new-1 starts here", }, { name: "boundary at end", text: "ends with old-1", oldID: "old-1", newID: "new-1", want: "ends with new-1", }, { name: "boundary punctuation", text: "See (old-1) and [old-1] or {old-1}", oldID: "old-1", newID: "new-1", want: "See (new-1) and [new-1] or {new-1}", }, { name: "no occurrence", text: "No match here", oldID: "old-1", newID: "new-1", want: "No match here", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := replaceBoundaryAware(tt.text, tt.oldID, tt.newID) if got != tt.want { t.Errorf("Got %q, want %q", got, tt.want) } }) } } func TestIsBoundary(t *testing.T) { boundaries := []byte{' ', '\t', '\n', '\r', ',', '.', '!', '?', ':', ';', '(', ')', '[', ']', '{', '}'} for _, b := range boundaries { if !isBoundary(b) { t.Errorf("Expected '%c' to be a boundary", b) } } notBoundaries := []byte{'a', 'Z', '0', '9', '-', '_'} for _, b := range notBoundaries { if isBoundary(b) { t.Errorf("Expected '%c' not to be a boundary", b) } } } func TestIsNumeric(t *testing.T) { tests := []struct { s string want bool }{ {"123", true}, {"0", true}, {"999", true}, {"abc", false}, {"12a", false}, {"", true}, // Empty string returns true (edge case in implementation) {"1.5", false}, } for _, tt := range tests { t.Run(tt.s, func(t *testing.T) { got := isNumeric(tt.s) if got != tt.want { t.Errorf("isNumeric(%q) = %v, want %v", tt.s, got, tt.want) } }) } } func stringPtr(s string) *string { return &s }