fix(tombstone): clear closed_at when converting closed issue to tombstone
When CreateTombstone was called on a closed issue, the CHECK constraint (status = closed) = (closed_at IS NOT NULL) was violated because closed_at was not cleared. Now setting closed_at = NULL in the UPDATE. Added regression test for creating tombstone from closed issue. Fixes: bd-fi05 Generated with Claude Code Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -873,9 +873,12 @@ func (s *SQLiteStorage) CreateTombstone(ctx context.Context, id string, actor st
|
|||||||
originalType := string(issue.IssueType)
|
originalType := string(issue.IssueType)
|
||||||
|
|
||||||
// Convert issue to tombstone
|
// Convert issue to tombstone
|
||||||
|
// Note: closed_at must be set to NULL because of CHECK constraint:
|
||||||
|
// (status = 'closed') = (closed_at IS NOT NULL)
|
||||||
_, err = tx.ExecContext(ctx, `
|
_, err = tx.ExecContext(ctx, `
|
||||||
UPDATE issues
|
UPDATE issues
|
||||||
SET status = ?,
|
SET status = ?,
|
||||||
|
closed_at = NULL,
|
||||||
deleted_at = ?,
|
deleted_at = ?,
|
||||||
deleted_by = ?,
|
deleted_by = ?,
|
||||||
delete_reason = ?,
|
delete_reason = ?,
|
||||||
|
|||||||
@@ -54,6 +54,58 @@ func TestCreateTombstone(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("create tombstone for closed issue", func(t *testing.T) {
|
||||||
|
// Regression test: closed issues have closed_at set, which must be
|
||||||
|
// cleared when creating tombstone due to CHECK constraint:
|
||||||
|
// (status = 'closed') = (closed_at IS NOT NULL)
|
||||||
|
issue := &types.Issue{
|
||||||
|
ID: "bd-closed-1",
|
||||||
|
Title: "Closed Issue",
|
||||||
|
Status: types.StatusOpen, // Create as open first
|
||||||
|
Priority: 1,
|
||||||
|
IssueType: types.TypeTask,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := store.CreateIssue(ctx, issue, "test"); err != nil {
|
||||||
|
t.Fatalf("Failed to create issue: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the issue to set closed_at
|
||||||
|
if err := store.CloseIssue(ctx, "bd-closed-1", "closing for test", "tester"); err != nil {
|
||||||
|
t.Fatalf("Failed to close issue: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify closed_at is set
|
||||||
|
closedIssue, err := store.GetIssue(ctx, "bd-closed-1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get closed issue: %v", err)
|
||||||
|
}
|
||||||
|
if closedIssue.ClosedAt == nil {
|
||||||
|
t.Fatal("closed_at should be set for closed issue")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create tombstone - this should work without constraint violation
|
||||||
|
if err := store.CreateTombstone(ctx, "bd-closed-1", "tester", "testing tombstone from closed"); err != nil {
|
||||||
|
t.Fatalf("CreateTombstone from closed issue failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify tombstone was created correctly
|
||||||
|
tombstone, err := store.GetIssue(ctx, "bd-closed-1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get tombstone: %v", err)
|
||||||
|
}
|
||||||
|
if tombstone.Status != types.StatusTombstone {
|
||||||
|
t.Errorf("Expected status=tombstone, got %s", tombstone.Status)
|
||||||
|
}
|
||||||
|
// closed_at should be nil for tombstone
|
||||||
|
if tombstone.ClosedAt != nil {
|
||||||
|
t.Error("closed_at should be nil for tombstone")
|
||||||
|
}
|
||||||
|
if tombstone.DeletedAt == nil {
|
||||||
|
t.Error("deleted_at should be set for tombstone")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("create tombstone for non-existent issue", func(t *testing.T) {
|
t.Run("create tombstone for non-existent issue", func(t *testing.T) {
|
||||||
err := store.CreateTombstone(ctx, "bd-999", "tester", "testing")
|
err := store.CreateTombstone(ctx, "bd-999", "tester", "testing")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user