fix: resolve P2 sync noise and cleanup issues
- 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>
This commit is contained in:
@@ -401,11 +401,15 @@ func validateJSONLIntegrity(ctx context.Context, jsonlPath string) (bool, error)
|
||||
jsonlData, err := os.ReadFile(jsonlPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// JSONL doesn't exist but we have a stored hash - clear export_hashes
|
||||
// JSONL doesn't exist but we have a stored hash - clear export_hashes and jsonl_file_hash
|
||||
fmt.Fprintf(os.Stderr, "⚠️ WARNING: JSONL file missing but export_hashes exist. Clearing export_hashes.\n")
|
||||
if err := store.ClearAllExportHashes(ctx); err != nil {
|
||||
return false, fmt.Errorf("failed to clear export_hashes: %w", err)
|
||||
}
|
||||
// Also clear jsonl_file_hash to prevent perpetual mismatch warnings (bd-admx)
|
||||
if err := store.SetJSONLFileHash(ctx, ""); err != nil {
|
||||
return false, fmt.Errorf("failed to clear jsonl_file_hash: %w", err)
|
||||
}
|
||||
return true, nil // Signal full export needed
|
||||
}
|
||||
return false, fmt.Errorf("failed to read JSONL file: %w", err)
|
||||
@@ -421,11 +425,15 @@ func validateJSONLIntegrity(ctx context.Context, jsonlPath string) (bool, error)
|
||||
fmt.Fprintf(os.Stderr, "⚠️ WARNING: JSONL file hash mismatch detected (bd-160)\n")
|
||||
fmt.Fprintf(os.Stderr, " This indicates JSONL and export_hashes are out of sync.\n")
|
||||
fmt.Fprintf(os.Stderr, " Clearing export_hashes to force full re-export.\n")
|
||||
|
||||
|
||||
// Clear export_hashes to force full re-export
|
||||
if err := store.ClearAllExportHashes(ctx); err != nil {
|
||||
return false, fmt.Errorf("failed to clear export_hashes: %w", err)
|
||||
}
|
||||
// Also clear jsonl_file_hash to prevent perpetual mismatch warnings (bd-admx)
|
||||
if err := store.SetJSONLFileHash(ctx, ""); err != nil {
|
||||
return false, fmt.Errorf("failed to clear jsonl_file_hash: %w", err)
|
||||
}
|
||||
return true, nil // Signal full export needed
|
||||
}
|
||||
|
||||
|
||||
@@ -685,7 +685,14 @@ func getDeletionsPath() string {
|
||||
// recordDeletion appends a deletion record to the deletions manifest.
|
||||
// This MUST be called BEFORE deleting from the database to ensure
|
||||
// deletion records are never lost.
|
||||
// After tombstone migration (bd-ffr9), this is a no-op since inline tombstones
|
||||
// are used instead of deletions.jsonl.
|
||||
func recordDeletion(id, deleteActor, reason string) error {
|
||||
// bd-ffr9: Skip writing to deletions.jsonl if tombstone migration is complete
|
||||
beadsDir := filepath.Dir(dbPath)
|
||||
if deletions.IsTombstoneMigrationComplete(beadsDir) {
|
||||
return nil
|
||||
}
|
||||
record := deletions.DeletionRecord{
|
||||
ID: id,
|
||||
Timestamp: time.Now().UTC(),
|
||||
@@ -698,7 +705,14 @@ func recordDeletion(id, deleteActor, reason string) error {
|
||||
// recordDeletions appends multiple deletion records to the deletions manifest.
|
||||
// This MUST be called BEFORE deleting from the database to ensure
|
||||
// deletion records are never lost.
|
||||
// After tombstone migration (bd-ffr9), this is a no-op since inline tombstones
|
||||
// are used instead of deletions.jsonl.
|
||||
func recordDeletions(ids []string, deleteActor, reason string) error {
|
||||
// bd-ffr9: Skip writing to deletions.jsonl if tombstone migration is complete
|
||||
beadsDir := filepath.Dir(dbPath)
|
||||
if deletions.IsTombstoneMigrationComplete(beadsDir) {
|
||||
return nil
|
||||
}
|
||||
path := getDeletionsPath()
|
||||
for _, id := range ids {
|
||||
record := deletions.DeletionRecord{
|
||||
|
||||
@@ -62,6 +62,39 @@ func TestRecordDeletion(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
@@ -16,12 +16,21 @@ import (
|
||||
// HydrateDeletionsManifest populates deletions.jsonl from git history.
|
||||
// It finds all issue IDs that were ever in the JSONL but are no longer present,
|
||||
// and adds them to the deletions manifest.
|
||||
// Note (bd-ffr9): After tombstone migration, this is a no-op since inline tombstones
|
||||
// are used instead of deletions.jsonl.
|
||||
func HydrateDeletionsManifest(path string) error {
|
||||
if err := validateBeadsWorkspace(path); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
beadsDir := filepath.Join(path, ".beads")
|
||||
|
||||
// bd-ffr9: Skip hydrating deletions.jsonl if tombstone migration is complete
|
||||
if deletions.IsTombstoneMigrationComplete(beadsDir) {
|
||||
fmt.Println(" Tombstone migration complete - skipping deletions.jsonl hydration")
|
||||
return nil
|
||||
}
|
||||
|
||||
// bd-6xd: issues.jsonl is the canonical filename
|
||||
jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
|
||||
|
||||
|
||||
@@ -100,12 +100,12 @@ func TestJSONLIntegrityValidation(t *testing.T) {
|
||||
if err := os.WriteFile(jsonlPath, []byte(`{"id":"bd-1","title":"Modified"}`+"\n"), 0644); err != nil {
|
||||
t.Fatalf("failed to modify JSONL: %v", err)
|
||||
}
|
||||
|
||||
|
||||
// Add an export hash to verify it gets cleared
|
||||
if err := testStore.SetExportHash(ctx, "bd-1", "dummy-hash"); err != nil {
|
||||
t.Fatalf("failed to set export hash: %v", err)
|
||||
}
|
||||
|
||||
|
||||
// Validate should detect mismatch and clear export_hashes
|
||||
needsFullExport, err := validateJSONLIntegrity(ctx, jsonlPath)
|
||||
if err != nil {
|
||||
@@ -114,7 +114,7 @@ func TestJSONLIntegrityValidation(t *testing.T) {
|
||||
if !needsFullExport {
|
||||
t.Fatalf("expected needsFullExport=true after clearing export_hashes")
|
||||
}
|
||||
|
||||
|
||||
// Verify export_hashes were cleared
|
||||
hash, err := testStore.GetExportHash(ctx, "bd-1")
|
||||
if err != nil {
|
||||
@@ -123,6 +123,15 @@ func TestJSONLIntegrityValidation(t *testing.T) {
|
||||
if hash != "" {
|
||||
t.Fatalf("expected export hash to be cleared, got %q", hash)
|
||||
}
|
||||
|
||||
// Verify jsonl_file_hash was also cleared (bd-admx fix)
|
||||
fileHash, err := testStore.GetJSONLFileHash(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get JSONL file hash: %v", err)
|
||||
}
|
||||
if fileHash != "" {
|
||||
t.Fatalf("expected jsonl_file_hash to be cleared to prevent perpetual warnings, got %q", fileHash)
|
||||
}
|
||||
})
|
||||
|
||||
// Test 3: Missing JSONL file
|
||||
@@ -131,17 +140,17 @@ func TestJSONLIntegrityValidation(t *testing.T) {
|
||||
if err := testStore.SetJSONLFileHash(ctx, "some-hash"); err != nil {
|
||||
t.Fatalf("failed to set JSONL file hash: %v", err)
|
||||
}
|
||||
|
||||
|
||||
// Add an export hash
|
||||
if err := testStore.SetExportHash(ctx, "bd-1", "dummy-hash"); err != nil {
|
||||
t.Fatalf("failed to set export hash: %v", err)
|
||||
}
|
||||
|
||||
|
||||
// Remove JSONL file
|
||||
if err := os.Remove(jsonlPath); err != nil {
|
||||
t.Fatalf("failed to remove JSONL: %v", err)
|
||||
}
|
||||
|
||||
|
||||
// Validate should detect missing file and clear export_hashes
|
||||
needsFullExport, err := validateJSONLIntegrity(ctx, jsonlPath)
|
||||
if err != nil {
|
||||
@@ -150,7 +159,7 @@ func TestJSONLIntegrityValidation(t *testing.T) {
|
||||
if !needsFullExport {
|
||||
t.Fatalf("expected needsFullExport=true after clearing export_hashes")
|
||||
}
|
||||
|
||||
|
||||
// Verify export_hashes were cleared
|
||||
hash, err := testStore.GetExportHash(ctx, "bd-1")
|
||||
if err != nil {
|
||||
@@ -159,6 +168,15 @@ func TestJSONLIntegrityValidation(t *testing.T) {
|
||||
if hash != "" {
|
||||
t.Fatalf("expected export hash to be cleared, got %q", hash)
|
||||
}
|
||||
|
||||
// Verify jsonl_file_hash was also cleared (bd-admx fix)
|
||||
fileHash, err := testStore.GetJSONLFileHash(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get JSONL file hash: %v", err)
|
||||
}
|
||||
if fileHash != "" {
|
||||
t.Fatalf("expected jsonl_file_hash to be cleared to prevent perpetual warnings, got %q", fileHash)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user