- bd-6pni: Auto-filter tombstoned issues with mismatched prefixes during import instead of failing. Tombstones from contributor PRs with different test prefixes are pollution and safe to ignore. - bd-ffr9: Stop recreating deletions.jsonl after tombstone migration. Added IsTombstoneMigrationComplete() check to all code paths that write to the legacy deletions manifest. - bd-admx: Fix perpetual "JSONL file hash mismatch" warning. Now clears both export_hashes AND jsonl_file_hash when mismatch detected, so the warning doesn't repeat. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
257 lines
7.1 KiB
Go
257 lines
7.1 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/steveyegge/beads/internal/deletions"
|
|
"github.com/steveyegge/beads/internal/storage/sqlite"
|
|
"github.com/steveyegge/beads/internal/types"
|
|
)
|
|
|
|
// TestRecordDeletion tests that recordDeletion creates deletion manifest entries
|
|
func TestRecordDeletion(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Set up dbPath so getDeletionsPath() works
|
|
oldDbPath := dbPath
|
|
dbPath = filepath.Join(tmpDir, "beads.db")
|
|
defer func() { dbPath = oldDbPath }()
|
|
|
|
// Create the .beads directory
|
|
if err := os.MkdirAll(tmpDir, 0750); err != nil {
|
|
t.Fatalf("failed to create directory: %v", err)
|
|
}
|
|
|
|
// Test recordDeletion
|
|
err := recordDeletion("test-abc", "test-user", "test reason")
|
|
if err != nil {
|
|
t.Fatalf("recordDeletion failed: %v", err)
|
|
}
|
|
|
|
// Verify the deletion was recorded
|
|
deletionsPath := getDeletionsPath()
|
|
result, err := deletions.LoadDeletions(deletionsPath)
|
|
if err != nil {
|
|
t.Fatalf("LoadDeletions failed: %v", err)
|
|
}
|
|
|
|
if len(result.Records) != 1 {
|
|
t.Fatalf("expected 1 deletion record, got %d", len(result.Records))
|
|
}
|
|
|
|
del, found := result.Records["test-abc"]
|
|
if !found {
|
|
t.Fatalf("deletion record for 'test-abc' not found")
|
|
}
|
|
|
|
if del.Actor != "test-user" {
|
|
t.Errorf("expected actor 'test-user', got '%s'", del.Actor)
|
|
}
|
|
|
|
if del.Reason != "test reason" {
|
|
t.Errorf("expected reason 'test reason', got '%s'", del.Reason)
|
|
}
|
|
|
|
// Timestamp should be recent (within last minute)
|
|
if time.Since(del.Timestamp) > time.Minute {
|
|
t.Errorf("timestamp seems too old: %v", del.Timestamp)
|
|
}
|
|
}
|
|
|
|
// TestRecordDeletion_SkipsAfterMigration tests that recordDeletion is a no-op after tombstone migration (bd-ffr9)
|
|
func TestRecordDeletion_SkipsAfterMigration(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Set up dbPath so getDeletionsPath() works
|
|
oldDbPath := dbPath
|
|
dbPath = filepath.Join(tmpDir, "beads.db")
|
|
defer func() { dbPath = oldDbPath }()
|
|
|
|
// Create the .beads directory
|
|
if err := os.MkdirAll(tmpDir, 0750); err != nil {
|
|
t.Fatalf("failed to create directory: %v", err)
|
|
}
|
|
|
|
// Create the .migrated marker file to indicate tombstone migration is complete
|
|
migratedPath := filepath.Join(tmpDir, "deletions.jsonl.migrated")
|
|
if err := os.WriteFile(migratedPath, []byte("{}"), 0644); err != nil {
|
|
t.Fatalf("failed to create migrated marker: %v", err)
|
|
}
|
|
|
|
// Test recordDeletion - should be a no-op
|
|
err := recordDeletion("test-abc", "test-user", "test reason")
|
|
if err != nil {
|
|
t.Fatalf("recordDeletion failed: %v", err)
|
|
}
|
|
|
|
// Verify deletions.jsonl was NOT created
|
|
deletionsPath := getDeletionsPath()
|
|
if _, err := os.Stat(deletionsPath); !os.IsNotExist(err) {
|
|
t.Error("deletions.jsonl should not be created after tombstone migration")
|
|
}
|
|
}
|
|
|
|
// TestRecordDeletions tests that recordDeletions creates multiple deletion manifest entries
|
|
func TestRecordDeletions(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Set up dbPath so getDeletionsPath() works
|
|
oldDbPath := dbPath
|
|
dbPath = filepath.Join(tmpDir, "beads.db")
|
|
defer func() { dbPath = oldDbPath }()
|
|
|
|
// Create the .beads directory
|
|
if err := os.MkdirAll(tmpDir, 0750); err != nil {
|
|
t.Fatalf("failed to create directory: %v", err)
|
|
}
|
|
|
|
// Test recordDeletions with multiple IDs
|
|
ids := []string{"test-abc", "test-def", "test-ghi"}
|
|
err := recordDeletions(ids, "batch-user", "batch cleanup")
|
|
if err != nil {
|
|
t.Fatalf("recordDeletions failed: %v", err)
|
|
}
|
|
|
|
// Verify the deletions were recorded
|
|
deletionsPath := getDeletionsPath()
|
|
result, err := deletions.LoadDeletions(deletionsPath)
|
|
if err != nil {
|
|
t.Fatalf("LoadDeletions failed: %v", err)
|
|
}
|
|
|
|
if len(result.Records) != 3 {
|
|
t.Fatalf("expected 3 deletion records, got %d", len(result.Records))
|
|
}
|
|
|
|
for _, id := range ids {
|
|
del, found := result.Records[id]
|
|
if !found {
|
|
t.Errorf("deletion record for '%s' not found", id)
|
|
continue
|
|
}
|
|
|
|
if del.Actor != "batch-user" {
|
|
t.Errorf("expected actor 'batch-user' for %s, got '%s'", id, del.Actor)
|
|
}
|
|
|
|
if del.Reason != "batch cleanup" {
|
|
t.Errorf("expected reason 'batch cleanup' for %s, got '%s'", id, del.Reason)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestGetActorWithGit tests actor sourcing logic
|
|
func TestGetActorWithGit(t *testing.T) {
|
|
// Save original actor value
|
|
oldActor := actor
|
|
defer func() { actor = oldActor }()
|
|
|
|
// Test case 1: actor is set from flag/env
|
|
actor = "flag-user"
|
|
result := getActorWithGit()
|
|
if result != "flag-user" {
|
|
t.Errorf("expected 'flag-user' when actor is set, got '%s'", result)
|
|
}
|
|
|
|
// Test case 2: actor is "unknown" - should try git config
|
|
actor = "unknown"
|
|
result = getActorWithGit()
|
|
// Can't test exact result since it depends on git config, but it shouldn't be empty
|
|
if result == "" {
|
|
t.Errorf("expected non-empty result when actor is 'unknown'")
|
|
}
|
|
|
|
// Test case 3: actor is empty - should try git config
|
|
actor = ""
|
|
result = getActorWithGit()
|
|
if result == "" {
|
|
t.Errorf("expected non-empty result when actor is empty")
|
|
}
|
|
}
|
|
|
|
// TestDeleteRecordingOrderOfOperations verifies deletion is recorded before DB delete
|
|
func TestDeleteRecordingOrderOfOperations(t *testing.T) {
|
|
ctx := context.Background()
|
|
tmpDir := t.TempDir()
|
|
|
|
// Set up dbPath
|
|
oldDbPath := dbPath
|
|
dbPath = filepath.Join(tmpDir, "beads.db")
|
|
defer func() { dbPath = oldDbPath }()
|
|
|
|
// Create database
|
|
testStore, err := sqlite.New(ctx, dbPath)
|
|
if err != nil {
|
|
t.Fatalf("failed to create database: %v", err)
|
|
}
|
|
defer testStore.Close()
|
|
|
|
// Initialize prefix
|
|
if err := testStore.SetConfig(ctx, "issue_prefix", "test"); err != nil {
|
|
t.Fatalf("failed to set prefix: %v", err)
|
|
}
|
|
|
|
// Create an issue
|
|
issue := &types.Issue{
|
|
ID: "test-delete-order",
|
|
Title: "Test Order of Operations",
|
|
Status: types.StatusOpen,
|
|
Priority: 1,
|
|
IssueType: types.TypeTask,
|
|
}
|
|
if err := testStore.CreateIssue(ctx, issue, "test"); err != nil {
|
|
t.Fatalf("failed to create issue: %v", err)
|
|
}
|
|
|
|
// Record deletion (simulating what delete command does)
|
|
if err := recordDeletion(issue.ID, "test-user", "order test"); err != nil {
|
|
t.Fatalf("recordDeletion failed: %v", err)
|
|
}
|
|
|
|
// Verify record was created BEFORE any DB changes
|
|
deletionsPath := getDeletionsPath()
|
|
result, err := deletions.LoadDeletions(deletionsPath)
|
|
if err != nil {
|
|
t.Fatalf("LoadDeletions failed: %v", err)
|
|
}
|
|
|
|
if _, found := result.Records[issue.ID]; !found {
|
|
t.Error("deletion record should exist before DB deletion")
|
|
}
|
|
|
|
// Now verify the issue still exists in DB (we only recorded, didn't delete)
|
|
existing, err := testStore.GetIssue(ctx, issue.ID)
|
|
if err != nil {
|
|
t.Fatalf("GetIssue failed: %v", err)
|
|
}
|
|
if existing == nil {
|
|
t.Error("issue should still exist in DB (we only recorded the deletion)")
|
|
}
|
|
|
|
// Now delete from DB
|
|
if err := testStore.DeleteIssue(ctx, issue.ID); err != nil {
|
|
t.Fatalf("DeleteIssue failed: %v", err)
|
|
}
|
|
|
|
// Verify both: deletion record exists AND issue is gone from DB
|
|
result, err = deletions.LoadDeletions(deletionsPath)
|
|
if err != nil {
|
|
t.Fatalf("LoadDeletions failed: %v", err)
|
|
}
|
|
if _, found := result.Records[issue.ID]; !found {
|
|
t.Error("deletion record should still exist after DB deletion")
|
|
}
|
|
|
|
existing, err = testStore.GetIssue(ctx, issue.ID)
|
|
if err != nil {
|
|
t.Fatalf("GetIssue failed: %v", err)
|
|
}
|
|
if existing != nil {
|
|
t.Error("issue should be gone from DB after deletion")
|
|
}
|
|
}
|