feat(mol): filter ephemeral issues from JSONL export (bd-687g)
Ephemeral issues should never be exported to issues.jsonl. They exist only in SQLite and are shared via .beads/redirect pointers. This prevents "zombie" issues from resurrecting after mol squash deletes them. Changes: - Filter ephemeral issues in autoflush, export, and multirepo_export - Add --summary flag to bd mol squash for agent-provided summaries - Fix DeleteIssue to also remove comments (missing cascade) - Add tests for ephemeral filtering and comment deletion 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -236,6 +236,57 @@ func TestDeleteIssue(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeleteIssueWithComments verifies that DeleteIssue also removes comments (bd-687g)
|
||||
func TestDeleteIssueWithComments(t *testing.T) {
|
||||
store := newTestStore(t, "file::memory:?mode=memory&cache=private")
|
||||
ctx := context.Background()
|
||||
|
||||
issue := &types.Issue{
|
||||
ID: "bd-1",
|
||||
Title: "Issue with Comments",
|
||||
Status: types.StatusOpen,
|
||||
Priority: 1,
|
||||
IssueType: types.TypeTask,
|
||||
}
|
||||
|
||||
if err := store.CreateIssue(ctx, issue, "test"); err != nil {
|
||||
t.Fatalf("Failed to create issue: %v", err)
|
||||
}
|
||||
|
||||
// Add a comment to the comments table (not events)
|
||||
if _, err := store.AddIssueComment(ctx, "bd-1", "test-author", "This is a test comment"); err != nil {
|
||||
t.Fatalf("Failed to add comment: %v", err)
|
||||
}
|
||||
|
||||
// Verify comment exists
|
||||
commentsMap, err := store.GetCommentsForIssues(ctx, []string{"bd-1"})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get comments: %v", err)
|
||||
}
|
||||
if len(commentsMap["bd-1"]) != 1 {
|
||||
t.Fatalf("Expected 1 comment, got %d", len(commentsMap["bd-1"]))
|
||||
}
|
||||
|
||||
// Delete the issue
|
||||
if err := store.DeleteIssue(ctx, "bd-1"); err != nil {
|
||||
t.Fatalf("DeleteIssue failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify issue deleted
|
||||
if issue, _ := store.GetIssue(ctx, "bd-1"); issue != nil {
|
||||
t.Error("Issue should be deleted")
|
||||
}
|
||||
|
||||
// Verify comments also deleted (should not leak)
|
||||
commentsMap, err = store.GetCommentsForIssues(ctx, []string{"bd-1"})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get comments after delete: %v", err)
|
||||
}
|
||||
if len(commentsMap["bd-1"]) != 0 {
|
||||
t.Errorf("Comments should be deleted, but found %d", len(commentsMap["bd-1"]))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildIDSet(t *testing.T) {
|
||||
ids := []string{"bd-1", "bd-2", "bd-3"}
|
||||
idSet := buildIDSet(ids)
|
||||
|
||||
@@ -50,6 +50,16 @@ func (s *SQLiteStorage) ExportToMultiRepo(ctx context.Context) (map[string]int,
|
||||
issue.Labels = labels
|
||||
}
|
||||
|
||||
// Filter out ephemeral issues - they should never be exported to JSONL (bd-687g)
|
||||
// Ephemeral issues exist only in SQLite and are shared via .beads/redirect, not JSONL.
|
||||
filtered := make([]*types.Issue, 0, len(allIssues))
|
||||
for _, issue := range allIssues {
|
||||
if !issue.Ephemeral {
|
||||
filtered = append(filtered, issue)
|
||||
}
|
||||
}
|
||||
allIssues = filtered
|
||||
|
||||
// Group issues by source_repo
|
||||
issuesByRepo := make(map[string][]*types.Issue)
|
||||
for _, issue := range allIssues {
|
||||
|
||||
@@ -1093,6 +1093,12 @@ func (s *SQLiteStorage) DeleteIssue(ctx context.Context, id string) error {
|
||||
return fmt.Errorf("failed to delete events: %w", err)
|
||||
}
|
||||
|
||||
// Delete comments (no FK cascade on this table) (bd-687g)
|
||||
_, err = tx.ExecContext(ctx, `DELETE FROM comments WHERE issue_id = ?`, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete comments: %w", err)
|
||||
}
|
||||
|
||||
// Delete from dirty_issues
|
||||
_, err = tx.ExecContext(ctx, `DELETE FROM dirty_issues WHERE issue_id = ?`, id)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user