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

@@ -37,6 +37,7 @@ func TestExportImport(t *testing.T) {
ctx := context.Background()
// Create test issues
now := time.Now()
issues := []*types.Issue{
{
ID: "test-1",
@@ -45,8 +46,8 @@ func TestExportImport(t *testing.T) {
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeBug,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
CreatedAt: now,
UpdatedAt: now,
},
{
ID: "test-2",
@@ -56,8 +57,8 @@ func TestExportImport(t *testing.T) {
Priority: 2,
IssueType: types.TypeFeature,
Assignee: "alice",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
CreatedAt: now,
UpdatedAt: now,
},
{
ID: "test-3",
@@ -66,8 +67,9 @@ func TestExportImport(t *testing.T) {
Status: types.StatusClosed,
Priority: 3,
IssueType: types.TypeTask,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
CreatedAt: now,
UpdatedAt: now,
ClosedAt: &now,
},
}
@@ -177,7 +179,7 @@ func TestExportImport(t *testing.T) {
// Import as update
updates := map[string]interface{}{
"title": issue.Title,
"status": issue.Status,
"status": string(issue.Status),
}
if err := store.UpdateIssue(ctx, issue.ID, updates, "test"); err != nil {
t.Fatalf("UpdateIssue failed: %v", err)

View File

@@ -7,6 +7,7 @@ import (
"fmt"
"os"
"sort"
"time"
"github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/storage/sqlite"
@@ -237,6 +238,18 @@ Behavior:
updated++
} else {
// Create new issue
// Normalize closed_at based on status before creating (enforce invariant)
if issue.Status == types.StatusClosed {
// Status is closed: ensure closed_at is set
if issue.ClosedAt == nil {
now := time.Now()
issue.ClosedAt = &now
}
} else {
// Status is not closed: ensure closed_at is NULL
issue.ClosedAt = nil
}
if err := store.CreateIssue(ctx, issue, "import"); err != nil {
fmt.Fprintf(os.Stderr, "Error creating issue %s: %v\n", issue.ID, err)
os.Exit(1)