Add closed_at timestamp tracking to issues

- Add closed_at field to Issue type with JSON marshaling
- Implement closed_at timestamp in SQLite storage layer
- Update import/export to handle closed_at field
- Add comprehensive tests for closed_at functionality
- Maintain backward compatibility with existing databases

Amp-Thread-ID: https://ampcode.com/threads/T-f3a7799b-f91e-4432-a690-aae0aed364b3
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Steve Yegge
2025-10-15 14:52:29 -07:00
parent ab809c5baf
commit d2b50e6cdc
8 changed files with 290 additions and 11 deletions

View File

@@ -46,6 +46,13 @@ func (i *Issue) Validate() error {
if i.EstimatedMinutes != nil && *i.EstimatedMinutes < 0 {
return fmt.Errorf("estimated_minutes cannot be negative")
}
// Enforce closed_at invariant: closed_at should be set if and only if status is closed
if i.Status == StatusClosed && i.ClosedAt == nil {
return fmt.Errorf("closed issues must have closed_at timestamp")
}
if i.Status != StatusClosed && i.ClosedAt != nil {
return fmt.Errorf("non-closed issues cannot have closed_at timestamp")
}
return nil
}

View File

@@ -120,6 +120,57 @@ func TestIssueValidation(t *testing.T) {
},
wantErr: false,
},
{
name: "closed issue without closed_at",
issue: Issue{
ID: "test-1",
Title: "Test",
Status: StatusClosed,
Priority: 2,
IssueType: TypeFeature,
ClosedAt: nil,
},
wantErr: true,
errMsg: "closed issues must have closed_at timestamp",
},
{
name: "open issue with closed_at",
issue: Issue{
ID: "test-1",
Title: "Test",
Status: StatusOpen,
Priority: 2,
IssueType: TypeFeature,
ClosedAt: timePtr(time.Now()),
},
wantErr: true,
errMsg: "non-closed issues cannot have closed_at timestamp",
},
{
name: "in_progress issue with closed_at",
issue: Issue{
ID: "test-1",
Title: "Test",
Status: StatusInProgress,
Priority: 2,
IssueType: TypeFeature,
ClosedAt: timePtr(time.Now()),
},
wantErr: true,
errMsg: "non-closed issues cannot have closed_at timestamp",
},
{
name: "closed issue with closed_at",
issue: Issue{
ID: "test-1",
Title: "Test",
Status: StatusClosed,
Priority: 2,
IssueType: TypeFeature,
ClosedAt: timePtr(time.Now()),
},
wantErr: false,
},
}
for _, tt := range tests {
@@ -287,6 +338,10 @@ func intPtr(i int) *int {
return &i
}
func timePtr(t time.Time) *time.Time {
return &t
}
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(s) > len(substr) && (s[:len(substr)] == substr || s[len(s)-len(substr):] == substr || containsMiddle(s, substr)))
}