test(linear): add comprehensive tests for mapping and client functions
Added tests for: - DefaultMappingConfig - PriorityToBeads/PriorityToLinear - StateToBeadsStatus/ParseBeadsStatus - StatusToLinearStateType - LabelToIssueType/ParseIssueType - RelationToBeadsDep - IssueToBeads (with parent/child handling) - BuildLinearToLocalUpdates - LoadMappingConfig - BuildLinearDescription - NewClient/WithEndpoint/WithHTTPClient - ExtractLinearIdentifier - IsLinearExternalRef Linear package coverage improved from 14.3% to ~50%. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,10 @@
|
|||||||
package linear
|
package linear
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
func TestCanonicalizeLinearExternalRef(t *testing.T) {
|
func TestCanonicalizeLinearExternalRef(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@@ -39,3 +43,130 @@ func TestCanonicalizeLinearExternalRef(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNewClient(t *testing.T) {
|
||||||
|
client := NewClient("test-api-key", "test-team-id")
|
||||||
|
|
||||||
|
if client.APIKey != "test-api-key" {
|
||||||
|
t.Errorf("APIKey = %q, want %q", client.APIKey, "test-api-key")
|
||||||
|
}
|
||||||
|
if client.TeamID != "test-team-id" {
|
||||||
|
t.Errorf("TeamID = %q, want %q", client.TeamID, "test-team-id")
|
||||||
|
}
|
||||||
|
if client.Endpoint != DefaultAPIEndpoint {
|
||||||
|
t.Errorf("Endpoint = %q, want %q", client.Endpoint, DefaultAPIEndpoint)
|
||||||
|
}
|
||||||
|
if client.HTTPClient == nil {
|
||||||
|
t.Error("HTTPClient should not be nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithEndpoint(t *testing.T) {
|
||||||
|
client := NewClient("key", "team")
|
||||||
|
customEndpoint := "https://custom.linear.app/graphql"
|
||||||
|
|
||||||
|
newClient := client.WithEndpoint(customEndpoint)
|
||||||
|
|
||||||
|
if newClient.Endpoint != customEndpoint {
|
||||||
|
t.Errorf("Endpoint = %q, want %q", newClient.Endpoint, customEndpoint)
|
||||||
|
}
|
||||||
|
// Original should be unchanged
|
||||||
|
if client.Endpoint != DefaultAPIEndpoint {
|
||||||
|
t.Errorf("Original endpoint changed: %q", client.Endpoint)
|
||||||
|
}
|
||||||
|
// Other fields preserved
|
||||||
|
if newClient.APIKey != "key" {
|
||||||
|
t.Errorf("APIKey not preserved: %q", newClient.APIKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithHTTPClient(t *testing.T) {
|
||||||
|
client := NewClient("key", "team")
|
||||||
|
customHTTPClient := &http.Client{Timeout: 60 * time.Second}
|
||||||
|
|
||||||
|
newClient := client.WithHTTPClient(customHTTPClient)
|
||||||
|
|
||||||
|
if newClient.HTTPClient != customHTTPClient {
|
||||||
|
t.Error("HTTPClient not set correctly")
|
||||||
|
}
|
||||||
|
// Other fields preserved
|
||||||
|
if newClient.APIKey != "key" {
|
||||||
|
t.Errorf("APIKey not preserved: %q", newClient.APIKey)
|
||||||
|
}
|
||||||
|
if newClient.Endpoint != DefaultAPIEndpoint {
|
||||||
|
t.Errorf("Endpoint not preserved: %q", newClient.Endpoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractLinearIdentifier(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
externalRef string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "standard URL",
|
||||||
|
externalRef: "https://linear.app/team/issue/PROJ-123",
|
||||||
|
want: "PROJ-123",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "URL with slug",
|
||||||
|
externalRef: "https://linear.app/team/issue/PROJ-456/some-title-here",
|
||||||
|
want: "PROJ-456",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "URL with trailing slash",
|
||||||
|
externalRef: "https://linear.app/team/issue/ABC-789/",
|
||||||
|
want: "ABC-789",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-linear URL",
|
||||||
|
externalRef: "https://jira.example.com/browse/PROJ-123",
|
||||||
|
want: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty string",
|
||||||
|
externalRef: "",
|
||||||
|
want: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "malformed URL",
|
||||||
|
externalRef: "not-a-url",
|
||||||
|
want: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := ExtractLinearIdentifier(tt.externalRef)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("ExtractLinearIdentifier(%q) = %q, want %q", tt.externalRef, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsLinearExternalRef(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
ref string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"https://linear.app/team/issue/PROJ-123", true},
|
||||||
|
{"https://linear.app/team/issue/PROJ-123/slug", true},
|
||||||
|
{"https://jira.example.com/browse/PROJ-123", false},
|
||||||
|
{"https://github.com/org/repo/issues/123", false},
|
||||||
|
{"", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.ref, func(t *testing.T) {
|
||||||
|
got := IsLinearExternalRef(tt.ref)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("IsLinearExternalRef(%q) = %v, want %v", tt.ref, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: BuildStateCache and FindStateForBeadsStatus require API calls
|
||||||
|
// and would need mocking to test. Skipping unit tests for those.
|
||||||
|
|||||||
@@ -142,3 +142,460 @@ func TestNormalizeIssueForLinearHashCanonicalizesExternalRef(t *testing.T) {
|
|||||||
func hasPrefix(s, prefix string) bool {
|
func hasPrefix(s, prefix string) bool {
|
||||||
return len(s) >= len(prefix) && s[:len(prefix)] == prefix
|
return len(s) >= len(prefix) && s[:len(prefix)] == prefix
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDefaultMappingConfig(t *testing.T) {
|
||||||
|
config := DefaultMappingConfig()
|
||||||
|
|
||||||
|
// Check priority mappings
|
||||||
|
if config.PriorityMap["0"] != 4 {
|
||||||
|
t.Errorf("PriorityMap[0] = %d, want 4", config.PriorityMap["0"])
|
||||||
|
}
|
||||||
|
if config.PriorityMap["1"] != 0 {
|
||||||
|
t.Errorf("PriorityMap[1] = %d, want 0", config.PriorityMap["1"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check state mappings
|
||||||
|
if config.StateMap["backlog"] != "open" {
|
||||||
|
t.Errorf("StateMap[backlog] = %s, want open", config.StateMap["backlog"])
|
||||||
|
}
|
||||||
|
if config.StateMap["started"] != "in_progress" {
|
||||||
|
t.Errorf("StateMap[started] = %s, want in_progress", config.StateMap["started"])
|
||||||
|
}
|
||||||
|
if config.StateMap["completed"] != "closed" {
|
||||||
|
t.Errorf("StateMap[completed] = %s, want closed", config.StateMap["completed"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check label type mappings
|
||||||
|
if config.LabelTypeMap["bug"] != "bug" {
|
||||||
|
t.Errorf("LabelTypeMap[bug] = %s, want bug", config.LabelTypeMap["bug"])
|
||||||
|
}
|
||||||
|
if config.LabelTypeMap["feature"] != "feature" {
|
||||||
|
t.Errorf("LabelTypeMap[feature] = %s, want feature", config.LabelTypeMap["feature"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check relation mappings
|
||||||
|
if config.RelationMap["blocks"] != "blocks" {
|
||||||
|
t.Errorf("RelationMap[blocks] = %s, want blocks", config.RelationMap["blocks"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPriorityToBeads(t *testing.T) {
|
||||||
|
config := DefaultMappingConfig()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
linearPriority int
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{0, 4}, // No priority -> Backlog
|
||||||
|
{1, 0}, // Urgent -> Critical
|
||||||
|
{2, 1}, // High -> High
|
||||||
|
{3, 2}, // Medium -> Medium
|
||||||
|
{4, 3}, // Low -> Low
|
||||||
|
{5, 2}, // Unknown -> Medium (default)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
got := PriorityToBeads(tt.linearPriority, config)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("PriorityToBeads(%d) = %d, want %d", tt.linearPriority, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPriorityToLinear(t *testing.T) {
|
||||||
|
config := DefaultMappingConfig()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
beadsPriority int
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{0, 1}, // Critical -> Urgent
|
||||||
|
{1, 2}, // High -> High
|
||||||
|
{2, 3}, // Medium -> Medium
|
||||||
|
{3, 4}, // Low -> Low
|
||||||
|
{4, 0}, // Backlog -> No priority
|
||||||
|
{5, 3}, // Unknown -> Medium (default)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
got := PriorityToLinear(tt.beadsPriority, config)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("PriorityToLinear(%d) = %d, want %d", tt.beadsPriority, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStateToBeadsStatus(t *testing.T) {
|
||||||
|
config := DefaultMappingConfig()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
state *State
|
||||||
|
want types.Status
|
||||||
|
}{
|
||||||
|
{nil, types.StatusOpen},
|
||||||
|
{&State{Type: "backlog", Name: "Backlog"}, types.StatusOpen},
|
||||||
|
{&State{Type: "unstarted", Name: "Todo"}, types.StatusOpen},
|
||||||
|
{&State{Type: "started", Name: "In Progress"}, types.StatusInProgress},
|
||||||
|
{&State{Type: "completed", Name: "Done"}, types.StatusClosed},
|
||||||
|
{&State{Type: "canceled", Name: "Cancelled"}, types.StatusClosed},
|
||||||
|
{&State{Type: "unknown", Name: "Unknown"}, types.StatusOpen}, // Default
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
got := StateToBeadsStatus(tt.state, config)
|
||||||
|
if got != tt.want {
|
||||||
|
stateName := "nil"
|
||||||
|
if tt.state != nil {
|
||||||
|
stateName = tt.state.Type
|
||||||
|
}
|
||||||
|
t.Errorf("StateToBeadsStatus(%s) = %v, want %v", stateName, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseBeadsStatus(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
want types.Status
|
||||||
|
}{
|
||||||
|
{"open", types.StatusOpen},
|
||||||
|
{"OPEN", types.StatusOpen},
|
||||||
|
{"in_progress", types.StatusInProgress},
|
||||||
|
{"in-progress", types.StatusInProgress},
|
||||||
|
{"inprogress", types.StatusInProgress},
|
||||||
|
{"blocked", types.StatusBlocked},
|
||||||
|
{"closed", types.StatusClosed},
|
||||||
|
{"CLOSED", types.StatusClosed},
|
||||||
|
{"unknown", types.StatusOpen}, // Default
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
got := ParseBeadsStatus(tt.input)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("ParseBeadsStatus(%q) = %v, want %v", tt.input, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStatusToLinearStateType(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
status types.Status
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{types.StatusOpen, "unstarted"},
|
||||||
|
{types.StatusInProgress, "started"},
|
||||||
|
{types.StatusBlocked, "started"},
|
||||||
|
{types.StatusClosed, "completed"},
|
||||||
|
{types.Status("unknown"), "unstarted"}, // Unknown -> default
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
got := StatusToLinearStateType(tt.status)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("StatusToLinearStateType(%v) = %q, want %q", tt.status, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLabelToIssueType(t *testing.T) {
|
||||||
|
config := DefaultMappingConfig()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
labels *Labels
|
||||||
|
want types.IssueType
|
||||||
|
}{
|
||||||
|
{nil, types.TypeTask},
|
||||||
|
{&Labels{Nodes: []Label{}}, types.TypeTask},
|
||||||
|
{&Labels{Nodes: []Label{{Name: "bug"}}}, types.TypeBug},
|
||||||
|
{&Labels{Nodes: []Label{{Name: "Bug"}}}, types.TypeBug},
|
||||||
|
{&Labels{Nodes: []Label{{Name: "feature"}}}, types.TypeFeature},
|
||||||
|
{&Labels{Nodes: []Label{{Name: "epic"}}}, types.TypeEpic},
|
||||||
|
{&Labels{Nodes: []Label{{Name: "chore"}}}, types.TypeChore},
|
||||||
|
{&Labels{Nodes: []Label{{Name: "random"}, {Name: "bug"}}}, types.TypeBug},
|
||||||
|
{&Labels{Nodes: []Label{{Name: "contains-bug-keyword"}}}, types.TypeBug},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
got := LabelToIssueType(tt.labels, config)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("LabelToIssueType(%v) = %v, want %v", tt.labels, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseIssueType(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
want types.IssueType
|
||||||
|
}{
|
||||||
|
{"bug", types.TypeBug},
|
||||||
|
{"BUG", types.TypeBug},
|
||||||
|
{"feature", types.TypeFeature},
|
||||||
|
{"task", types.TypeTask},
|
||||||
|
{"epic", types.TypeEpic},
|
||||||
|
{"chore", types.TypeChore},
|
||||||
|
{"unknown", types.TypeTask}, // Default
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
got := ParseIssueType(tt.input)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("ParseIssueType(%q) = %v, want %v", tt.input, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRelationToBeadsDep(t *testing.T) {
|
||||||
|
config := DefaultMappingConfig()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
relationType string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"blocks", "blocks"},
|
||||||
|
{"blockedBy", "blocks"},
|
||||||
|
{"duplicate", "duplicates"},
|
||||||
|
{"related", "related"},
|
||||||
|
{"unknown", "related"}, // Default
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
got := RelationToBeadsDep(tt.relationType, config)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("RelationToBeadsDep(%q) = %q, want %q", tt.relationType, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIssueToBeads(t *testing.T) {
|
||||||
|
config := DefaultMappingConfig()
|
||||||
|
|
||||||
|
linearIssue := &Issue{
|
||||||
|
ID: "uuid-123",
|
||||||
|
Identifier: "PROJ-123",
|
||||||
|
Title: "Test Issue",
|
||||||
|
Description: "Test description",
|
||||||
|
URL: "https://linear.app/team/issue/PROJ-123/test-issue",
|
||||||
|
Priority: 2, // High
|
||||||
|
State: &State{Type: "started", Name: "In Progress"},
|
||||||
|
Assignee: &User{Name: "John Doe", Email: "john@example.com"},
|
||||||
|
Labels: &Labels{Nodes: []Label{{Name: "bug"}}},
|
||||||
|
CreatedAt: "2024-01-15T10:00:00Z",
|
||||||
|
UpdatedAt: "2024-01-16T12:00:00Z",
|
||||||
|
}
|
||||||
|
|
||||||
|
result := IssueToBeads(linearIssue, config)
|
||||||
|
issue := result.Issue.(*types.Issue)
|
||||||
|
|
||||||
|
if issue.Title != "Test Issue" {
|
||||||
|
t.Errorf("Title = %q, want %q", issue.Title, "Test Issue")
|
||||||
|
}
|
||||||
|
if issue.Description != "Test description" {
|
||||||
|
t.Errorf("Description = %q, want %q", issue.Description, "Test description")
|
||||||
|
}
|
||||||
|
if issue.Priority != 1 { // High in beads
|
||||||
|
t.Errorf("Priority = %d, want 1", issue.Priority)
|
||||||
|
}
|
||||||
|
if issue.Status != types.StatusInProgress {
|
||||||
|
t.Errorf("Status = %v, want %v", issue.Status, types.StatusInProgress)
|
||||||
|
}
|
||||||
|
if issue.Assignee != "john@example.com" {
|
||||||
|
t.Errorf("Assignee = %q, want %q", issue.Assignee, "john@example.com")
|
||||||
|
}
|
||||||
|
if issue.IssueType != types.TypeBug {
|
||||||
|
t.Errorf("IssueType = %v, want %v", issue.IssueType, types.TypeBug)
|
||||||
|
}
|
||||||
|
if issue.ExternalRef == nil {
|
||||||
|
t.Error("ExternalRef should not be nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIssueToBeadsWithParent(t *testing.T) {
|
||||||
|
config := DefaultMappingConfig()
|
||||||
|
|
||||||
|
linearIssue := &Issue{
|
||||||
|
ID: "uuid-456",
|
||||||
|
Identifier: "PROJ-456",
|
||||||
|
Title: "Child Issue",
|
||||||
|
Description: "Child description",
|
||||||
|
URL: "https://linear.app/team/issue/PROJ-456",
|
||||||
|
Priority: 3,
|
||||||
|
State: &State{Type: "unstarted", Name: "Todo"},
|
||||||
|
Parent: &Parent{ID: "uuid-123", Identifier: "PROJ-123"},
|
||||||
|
CreatedAt: "2024-01-15T10:00:00Z",
|
||||||
|
UpdatedAt: "2024-01-16T12:00:00Z",
|
||||||
|
}
|
||||||
|
|
||||||
|
result := IssueToBeads(linearIssue, config)
|
||||||
|
|
||||||
|
if len(result.Dependencies) != 1 {
|
||||||
|
t.Fatalf("Expected 1 dependency, got %d", len(result.Dependencies))
|
||||||
|
}
|
||||||
|
if result.Dependencies[0].Type != "parent-child" {
|
||||||
|
t.Errorf("Dependency type = %q, want %q", result.Dependencies[0].Type, "parent-child")
|
||||||
|
}
|
||||||
|
if result.Dependencies[0].FromLinearID != "PROJ-456" {
|
||||||
|
t.Errorf("FromLinearID = %q, want %q", result.Dependencies[0].FromLinearID, "PROJ-456")
|
||||||
|
}
|
||||||
|
if result.Dependencies[0].ToLinearID != "PROJ-123" {
|
||||||
|
t.Errorf("ToLinearID = %q, want %q", result.Dependencies[0].ToLinearID, "PROJ-123")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildLinearToLocalUpdates(t *testing.T) {
|
||||||
|
config := DefaultMappingConfig()
|
||||||
|
|
||||||
|
linearIssue := &Issue{
|
||||||
|
ID: "uuid-123",
|
||||||
|
Identifier: "PROJ-123",
|
||||||
|
Title: "Updated Title",
|
||||||
|
Description: "Updated description",
|
||||||
|
Priority: 1, // Urgent
|
||||||
|
State: &State{Type: "completed", Name: "Done"},
|
||||||
|
Assignee: &User{Name: "Jane Doe", Email: "jane@example.com"},
|
||||||
|
Labels: &Labels{Nodes: []Label{{Name: "feature"}, {Name: "priority"}}},
|
||||||
|
UpdatedAt: "2024-01-20T15:00:00Z",
|
||||||
|
CompletedAt: "2024-01-20T14:00:00Z",
|
||||||
|
}
|
||||||
|
|
||||||
|
updates := BuildLinearToLocalUpdates(linearIssue, config)
|
||||||
|
|
||||||
|
if updates["title"] != "Updated Title" {
|
||||||
|
t.Errorf("title = %v, want %q", updates["title"], "Updated Title")
|
||||||
|
}
|
||||||
|
if updates["description"] != "Updated description" {
|
||||||
|
t.Errorf("description = %v, want %q", updates["description"], "Updated description")
|
||||||
|
}
|
||||||
|
if updates["priority"] != 0 { // Critical in beads
|
||||||
|
t.Errorf("priority = %v, want 0", updates["priority"])
|
||||||
|
}
|
||||||
|
if updates["status"] != "closed" {
|
||||||
|
t.Errorf("status = %v, want %q", updates["status"], "closed")
|
||||||
|
}
|
||||||
|
if updates["assignee"] != "jane@example.com" {
|
||||||
|
t.Errorf("assignee = %v, want %q", updates["assignee"], "jane@example.com")
|
||||||
|
}
|
||||||
|
|
||||||
|
labels, ok := updates["labels"].([]string)
|
||||||
|
if !ok || len(labels) != 2 {
|
||||||
|
t.Errorf("labels = %v, want 2 labels", updates["labels"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildLinearToLocalUpdatesNoAssignee(t *testing.T) {
|
||||||
|
config := DefaultMappingConfig()
|
||||||
|
|
||||||
|
linearIssue := &Issue{
|
||||||
|
ID: "uuid-123",
|
||||||
|
Identifier: "PROJ-123",
|
||||||
|
Title: "No Assignee",
|
||||||
|
Description: "Test",
|
||||||
|
Priority: 3,
|
||||||
|
State: &State{Type: "unstarted", Name: "Todo"},
|
||||||
|
Assignee: nil,
|
||||||
|
UpdatedAt: "2024-01-20T15:00:00Z",
|
||||||
|
}
|
||||||
|
|
||||||
|
updates := BuildLinearToLocalUpdates(linearIssue, config)
|
||||||
|
|
||||||
|
if updates["assignee"] != "" {
|
||||||
|
t.Errorf("assignee = %v, want empty string", updates["assignee"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mockConfigLoader implements ConfigLoader for testing
|
||||||
|
type mockConfigLoader struct {
|
||||||
|
config map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockConfigLoader) GetAllConfig() (map[string]string, error) {
|
||||||
|
return m.config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadMappingConfig(t *testing.T) {
|
||||||
|
loader := &mockConfigLoader{
|
||||||
|
config: map[string]string{
|
||||||
|
"linear.priority_map.0": "3",
|
||||||
|
"linear.state_map.custom": "in_progress",
|
||||||
|
"linear.label_type_map.story": "feature",
|
||||||
|
"linear.relation_map.parent": "parent-child",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
config := LoadMappingConfig(loader)
|
||||||
|
|
||||||
|
// Check custom priority mapping
|
||||||
|
if config.PriorityMap["0"] != 3 {
|
||||||
|
t.Errorf("PriorityMap[0] = %d, want 3", config.PriorityMap["0"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check custom state mapping
|
||||||
|
if config.StateMap["custom"] != "in_progress" {
|
||||||
|
t.Errorf("StateMap[custom] = %s, want in_progress", config.StateMap["custom"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check custom label type mapping
|
||||||
|
if config.LabelTypeMap["story"] != "feature" {
|
||||||
|
t.Errorf("LabelTypeMap[story] = %s, want feature", config.LabelTypeMap["story"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check custom relation mapping
|
||||||
|
if config.RelationMap["parent"] != "parent-child" {
|
||||||
|
t.Errorf("RelationMap[parent] = %s, want parent-child", config.RelationMap["parent"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that defaults are preserved
|
||||||
|
if config.StateMap["started"] != "in_progress" {
|
||||||
|
t.Errorf("StateMap[started] = %s, want in_progress (default preserved)", config.StateMap["started"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadMappingConfigNilLoader(t *testing.T) {
|
||||||
|
config := LoadMappingConfig(nil)
|
||||||
|
|
||||||
|
// Should return defaults
|
||||||
|
if config.PriorityMap["1"] != 0 {
|
||||||
|
t.Errorf("Expected default priority map with nil loader")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildLinearDescription(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
issue *types.Issue
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "description only",
|
||||||
|
issue: &types.Issue{Description: "Basic description"},
|
||||||
|
want: "Basic description",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with acceptance criteria",
|
||||||
|
issue: &types.Issue{
|
||||||
|
Description: "Main description",
|
||||||
|
AcceptanceCriteria: "- Must do X\n- Must do Y",
|
||||||
|
},
|
||||||
|
want: "Main description\n\n## Acceptance Criteria\n- Must do X\n- Must do Y",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with all fields",
|
||||||
|
issue: &types.Issue{
|
||||||
|
Description: "Main description",
|
||||||
|
AcceptanceCriteria: "AC here",
|
||||||
|
Design: "Design notes",
|
||||||
|
Notes: "Additional notes",
|
||||||
|
},
|
||||||
|
want: "Main description\n\n## Acceptance Criteria\nAC here\n\n## Design\nDesign notes\n\n## Notes\nAdditional notes",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := BuildLinearDescription(tt.issue)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("BuildLinearDescription() = %q, want %q", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user