Implement Jira issue timestamp comparison for sync (bd-0qx5)

Add actual timestamp comparison to detect conflicts during Jira sync:
- Add fetchJiraIssueTimestamp() to fetch a single issue's updated timestamp from Jira REST API
- Add extractJiraKey() to parse Jira issue key from external_ref URLs
- Add parseJiraTimestamp() to parse Jira's ISO 8601 timestamp format
- Update detectJiraConflicts() to fetch and compare Jira timestamps instead of marking all locally-updated issues as conflicts
- Update resolveConflictsByTimestamp() to show actual timestamp comparison results
- Update reimportConflicts() to display both local and Jira timestamps

Now only issues that have been modified on BOTH sides since the last sync are reported as conflicts, reducing false positives.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-30 15:59:52 -08:00
parent 9ee4fb50e2
commit aa2c66c4f7
2 changed files with 325 additions and 13 deletions

View File

@@ -181,3 +181,114 @@ func TestPushStats(t *testing.T) {
t.Errorf("expected Errors to be 2, got %d", stats.Errors)
}
}
func TestExtractJiraKey(t *testing.T) {
tests := []struct {
name string
externalRef string
want string
}{
{
name: "standard Jira Cloud URL",
externalRef: "https://company.atlassian.net/browse/PROJ-123",
want: "PROJ-123",
},
{
name: "Jira Server URL",
externalRef: "https://jira.company.com/browse/ISSUE-456",
want: "ISSUE-456",
},
{
name: "URL with trailing path",
externalRef: "https://company.atlassian.net/browse/ABC-789/some/path",
want: "ABC-789/some/path",
},
{
name: "no browse pattern",
externalRef: "https://github.com/org/repo/issues/123",
want: "",
},
{
name: "empty string",
externalRef: "",
want: "",
},
{
name: "only browse",
externalRef: "https://example.com/browse/",
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := extractJiraKey(tt.externalRef)
if got != tt.want {
t.Errorf("extractJiraKey(%q) = %q, want %q", tt.externalRef, got, tt.want)
}
})
}
}
func TestParseJiraTimestamp(t *testing.T) {
tests := []struct {
name string
timestamp string
wantErr bool
wantYear int
}{
{
name: "standard Jira Cloud format with milliseconds",
timestamp: "2024-01-15T10:30:00.000+0000",
wantErr: false,
wantYear: 2024,
},
{
name: "Jira format with Z suffix",
timestamp: "2024-01-15T10:30:00.000Z",
wantErr: false,
wantYear: 2024,
},
{
name: "without milliseconds",
timestamp: "2024-01-15T10:30:00+0000",
wantErr: false,
wantYear: 2024,
},
{
name: "RFC3339 format",
timestamp: "2024-01-15T10:30:00Z",
wantErr: false,
wantYear: 2024,
},
{
name: "empty string",
timestamp: "",
wantErr: true,
},
{
name: "invalid format",
timestamp: "not-a-timestamp",
wantErr: true,
},
{
name: "with negative timezone offset",
timestamp: "2024-06-15T10:30:00.000-0500",
wantErr: false,
wantYear: 2024,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseJiraTimestamp(tt.timestamp)
if (err != nil) != tt.wantErr {
t.Errorf("parseJiraTimestamp(%q) error = %v, wantErr %v", tt.timestamp, err, tt.wantErr)
return
}
if !tt.wantErr && got.Year() != tt.wantYear {
t.Errorf("parseJiraTimestamp(%q) year = %d, want %d", tt.timestamp, got.Year(), tt.wantYear)
}
})
}
}