Fix bd-in7q: prevent migrate-tombstones from corrupting deletions manifest (#554)
Root cause: bd doctor hydrate was re-adding migrated tombstones to the deletions manifest because getCurrentJSONLIDs() included all issues, including tombstones. When compared against git history, tombstones appeared as 'deleted' and were incorrectly added to the manifest as new deletions, corrupting the database on next sync. Fix: Skip tombstone-status issues in getCurrentJSONLIDs() so they don't participate in deletion detection. Tombstones represent already-recorded deletions/migrations and shouldn't be treated as active issues. Changes: - cmd/bd/doctor/fix/deletions.go: Skip tombstones in getCurrentJSONLIDs() - cmd/bd/doctor/fix/deletions_test.go: New tests for tombstone skipping - cmd/bd/migrate_tombstones_test.go: Test that tombstones are valid This fixes the bug where 'bd migrate-tombstones' followed by 'bd sync' would add thousands of deletion records with author 'bd-doctor-hydrate'
This commit is contained in:
42
.beads/.gitignore
vendored
42
.beads/.gitignore
vendored
@@ -1,20 +1,32 @@
|
||||
# Ignore all .beads/ contents by default (local workspace files)
|
||||
# Only files explicitly whitelisted below will be tracked in git
|
||||
*
|
||||
# SQLite databases
|
||||
*.db
|
||||
*.db?*
|
||||
*.db-journal
|
||||
*.db-wal
|
||||
*.db-shm
|
||||
|
||||
# === Files tracked in git (shared across clones) ===
|
||||
# Daemon runtime files
|
||||
daemon.lock
|
||||
daemon.log
|
||||
daemon.pid
|
||||
bd.sock
|
||||
|
||||
# This gitignore file itself
|
||||
!.gitignore
|
||||
# Local version tracking (prevents upgrade notification spam after git ops)
|
||||
.local_version
|
||||
|
||||
# Issue data in JSONL format (the main data file)
|
||||
# Legacy database files
|
||||
db.sqlite
|
||||
bd.db
|
||||
|
||||
# Merge artifacts (temporary files from 3-way merge)
|
||||
beads.base.jsonl
|
||||
beads.base.meta.json
|
||||
beads.left.jsonl
|
||||
beads.left.meta.json
|
||||
beads.right.jsonl
|
||||
beads.right.meta.json
|
||||
|
||||
# Keep JSONL exports and config (source of truth for git)
|
||||
!issues.jsonl
|
||||
|
||||
# Repository metadata (database name, JSONL filename)
|
||||
!metadata.json
|
||||
|
||||
# Configuration template (sync branch, integrations)
|
||||
!config.yaml
|
||||
|
||||
# Documentation for contributors
|
||||
!README.md
|
||||
!config.json
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
27
.beads/deletions.jsonl.migrated
Normal file
27
.beads/deletions.jsonl.migrated
Normal file
@@ -0,0 +1,27 @@
|
||||
{"id":"bd-3pd","ts":"2025-12-02T05:05:53.732197Z","by":"stevey","reason":"batch delete"}
|
||||
{"id":"bd-ksc","ts":"2025-12-02T05:05:53.742343Z","by":"stevey","reason":"batch delete"}
|
||||
{"id":"bd-360","ts":"2025-12-02T05:05:53.74642Z","by":"stevey","reason":"batch delete"}
|
||||
{"id":"bd-cb64c226.1","ts":"2025-12-14T06:42:56.65452Z","by":"stevey","reason":"batch delete"}
|
||||
{"id":"bd-cb64c226.6","ts":"2025-12-14T06:42:56.660775Z","by":"stevey","reason":"batch delete"}
|
||||
{"id":"bd-cb64c226.8","ts":"2025-12-14T06:42:56.66501Z","by":"stevey","reason":"batch delete"}
|
||||
{"id":"bd-cb64c226.9","ts":"2025-12-14T06:42:56.669731Z","by":"stevey","reason":"batch delete"}
|
||||
{"id":"bd-cb64c226.10","ts":"2025-12-14T06:42:56.674952Z","by":"stevey","reason":"batch delete"}
|
||||
{"id":"bd-cb64c226.12","ts":"2025-12-14T06:42:56.678754Z","by":"stevey","reason":"batch delete"}
|
||||
{"id":"bd-cb64c226.13","ts":"2025-12-14T06:42:56.682961Z","by":"stevey","reason":"batch delete"}
|
||||
{"id":"bd-cbed9619.1","ts":"2025-12-14T06:42:56.686774Z","by":"stevey","reason":"batch delete"}
|
||||
{"id":"bd-cbed9619.2","ts":"2025-12-14T06:42:56.690962Z","by":"stevey","reason":"batch delete"}
|
||||
{"id":"bd-cbed9619.3","ts":"2025-12-14T06:42:56.694789Z","by":"stevey","reason":"batch delete"}
|
||||
{"id":"bd-cbed9619.4","ts":"2025-12-14T06:42:56.698915Z","by":"stevey","reason":"batch delete"}
|
||||
{"id":"bd-cbed9619.5","ts":"2025-12-14T06:42:56.702767Z","by":"stevey","reason":"batch delete"}
|
||||
{"id":"bd-cb64c226.1","ts":"2025-12-14T06:43:28.945078Z","by":"stevey","reason":"batch delete"}
|
||||
{"id":"bd-cb64c226.6","ts":"2025-12-14T06:43:28.951574Z","by":"stevey","reason":"batch delete"}
|
||||
{"id":"bd-cb64c226.8","ts":"2025-12-14T06:43:28.956558Z","by":"stevey","reason":"batch delete"}
|
||||
{"id":"bd-cb64c226.9","ts":"2025-12-14T06:43:28.961447Z","by":"stevey","reason":"batch delete"}
|
||||
{"id":"bd-cb64c226.10","ts":"2025-12-14T06:43:28.96553Z","by":"stevey","reason":"batch delete"}
|
||||
{"id":"bd-cb64c226.12","ts":"2025-12-14T06:43:28.969449Z","by":"stevey","reason":"batch delete"}
|
||||
{"id":"bd-cb64c226.13","ts":"2025-12-14T06:43:28.973591Z","by":"stevey","reason":"batch delete"}
|
||||
{"id":"bd-cbed9619.1","ts":"2025-12-14T06:43:28.977362Z","by":"stevey","reason":"batch delete"}
|
||||
{"id":"bd-cbed9619.2","ts":"2025-12-14T06:43:28.981423Z","by":"stevey","reason":"batch delete"}
|
||||
{"id":"bd-cbed9619.3","ts":"2025-12-14T06:43:28.985436Z","by":"stevey","reason":"batch delete"}
|
||||
{"id":"bd-cbed9619.4","ts":"2025-12-14T06:43:28.989612Z","by":"stevey","reason":"batch delete"}
|
||||
{"id":"bd-cbed9619.5","ts":"2025-12-14T06:43:28.993408Z","by":"stevey","reason":"batch delete"}
|
||||
1615
.beads/issues.jsonl
1615
.beads/issues.jsonl
File diff suppressed because one or more lines are too long
@@ -2,4 +2,4 @@
|
||||
"database": "beads.db",
|
||||
"jsonl_export": "issues.jsonl",
|
||||
"last_bd_version": "0.27.2"
|
||||
}
|
||||
}
|
||||
@@ -118,12 +118,15 @@ func getCurrentJSONLIDs(jsonlPath string) (map[string]bool, error) {
|
||||
}
|
||||
|
||||
var issue struct {
|
||||
ID string `json:"id"`
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
if err := json.Unmarshal(line, &issue); err != nil {
|
||||
continue
|
||||
}
|
||||
if issue.ID != "" {
|
||||
// Skip tombstones - they represent migrated deletions and shouldn't
|
||||
// be re-added to the deletions manifest (bd-in7q fix)
|
||||
if issue.ID != "" && issue.Status != "tombstone" {
|
||||
ids[issue.ID] = true
|
||||
}
|
||||
}
|
||||
|
||||
148
cmd/bd/doctor/fix/deletions_test.go
Normal file
148
cmd/bd/doctor/fix/deletions_test.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package fix
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
)
|
||||
|
||||
func TestGetCurrentJSONLIDs_SkipsTombstones(t *testing.T) {
|
||||
// Setup: Create temp file with mix of normal issues and tombstones
|
||||
tmpDir := t.TempDir()
|
||||
jsonlPath := filepath.Join(tmpDir, "issues.jsonl")
|
||||
|
||||
// Create a JSONL file with both normal issues and tombstones
|
||||
issues := []*types.Issue{
|
||||
{
|
||||
ID: "bd-abc",
|
||||
Title: "Normal issue",
|
||||
Status: types.StatusOpen,
|
||||
},
|
||||
{
|
||||
ID: "bd-def",
|
||||
Title: "(deleted)",
|
||||
Status: types.StatusTombstone,
|
||||
DeletedBy: "test-user",
|
||||
},
|
||||
{
|
||||
ID: "bd-ghi",
|
||||
Title: "Another normal issue",
|
||||
Status: types.StatusOpen,
|
||||
},
|
||||
{
|
||||
ID: "bd-jkl",
|
||||
Title: "(deleted)",
|
||||
Status: types.StatusTombstone,
|
||||
DeletedBy: "test-user",
|
||||
},
|
||||
}
|
||||
|
||||
file, err := os.Create(jsonlPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test JSONL file: %v", err)
|
||||
}
|
||||
encoder := json.NewEncoder(file)
|
||||
for _, issue := range issues {
|
||||
if err := encoder.Encode(issue); err != nil {
|
||||
file.Close()
|
||||
t.Fatalf("Failed to write issue to JSONL: %v", err)
|
||||
}
|
||||
}
|
||||
file.Close()
|
||||
|
||||
// Call getCurrentJSONLIDs
|
||||
ids, err := getCurrentJSONLIDs(jsonlPath)
|
||||
if err != nil {
|
||||
t.Fatalf("getCurrentJSONLIDs failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify: Should only contain non-tombstone IDs
|
||||
expectedIDs := map[string]bool{
|
||||
"bd-abc": true,
|
||||
"bd-ghi": true,
|
||||
}
|
||||
|
||||
if len(ids) != len(expectedIDs) {
|
||||
t.Errorf("Expected %d IDs, got %d. IDs: %v", len(expectedIDs), len(ids), ids)
|
||||
}
|
||||
|
||||
for expectedID := range expectedIDs {
|
||||
if !ids[expectedID] {
|
||||
t.Errorf("Expected ID %s to be present", expectedID)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify tombstones are NOT included
|
||||
if ids["bd-def"] {
|
||||
t.Error("Tombstone bd-def should not be included in current IDs")
|
||||
}
|
||||
if ids["bd-jkl"] {
|
||||
t.Error("Tombstone bd-jkl should not be included in current IDs")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCurrentJSONLIDs_HandlesEmptyFile(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
jsonlPath := filepath.Join(tmpDir, "issues.jsonl")
|
||||
|
||||
// Create empty file
|
||||
if _, err := os.Create(jsonlPath); err != nil {
|
||||
t.Fatalf("Failed to create empty file: %v", err)
|
||||
}
|
||||
|
||||
ids, err := getCurrentJSONLIDs(jsonlPath)
|
||||
if err != nil {
|
||||
t.Fatalf("getCurrentJSONLIDs failed: %v", err)
|
||||
}
|
||||
|
||||
if len(ids) != 0 {
|
||||
t.Errorf("Expected 0 IDs from empty file, got %d", len(ids))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCurrentJSONLIDs_HandlesMissingFile(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
nonexistentPath := filepath.Join(tmpDir, "nonexistent.jsonl")
|
||||
|
||||
ids, err := getCurrentJSONLIDs(nonexistentPath)
|
||||
if err != nil {
|
||||
t.Fatalf("getCurrentJSONLIDs should handle missing file gracefully: %v", err)
|
||||
}
|
||||
|
||||
if len(ids) != 0 {
|
||||
t.Errorf("Expected 0 IDs from missing file, got %d", len(ids))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCurrentJSONLIDs_SkipsInvalidJSON(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
jsonlPath := filepath.Join(tmpDir, "issues.jsonl")
|
||||
|
||||
// Write mixed valid and invalid JSON lines
|
||||
content := `{"id":"bd-valid","status":"open"}
|
||||
invalid json line
|
||||
{"id":"bd-another","status":"open"}
|
||||
`
|
||||
if err := os.WriteFile(jsonlPath, []byte(content), 0600); err != nil {
|
||||
t.Fatalf("Failed to write test file: %v", err)
|
||||
}
|
||||
|
||||
ids, err := getCurrentJSONLIDs(jsonlPath)
|
||||
if err != nil {
|
||||
t.Fatalf("getCurrentJSONLIDs failed: %v", err)
|
||||
}
|
||||
|
||||
if len(ids) != 2 {
|
||||
t.Errorf("Expected 2 valid IDs, got %d. IDs: %v", len(ids), ids)
|
||||
}
|
||||
if !ids["bd-valid"] || !ids["bd-another"] {
|
||||
t.Error("Expected to parse both valid issues despite invalid line in between")
|
||||
}
|
||||
}
|
||||
|
||||
// Note: Full integration test for HydrateDeletionsManifest would require git repo setup.
|
||||
// The unit tests above verify the core fix (skipping tombstones in getCurrentJSONLIDs).
|
||||
// Integration tests are handled in migrate_tombstones_test.go with full sync cycle.
|
||||
108
cmd/bd/doctor/fix/docs.md
Normal file
108
cmd/bd/doctor/fix/docs.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# Noridoc: cmd/bd/doctor/fix
|
||||
|
||||
Path: @/cmd/bd/doctor/fix
|
||||
|
||||
### Overview
|
||||
|
||||
The `cmd/bd/doctor/fix` directory contains automated remediation functions for issues detected by the `bd doctor` command. Each module handles a specific category of issues (deletions manifest, database config, sync branch, etc.) and provides functions to automatically fix problems found in beads workspaces.
|
||||
|
||||
### How it fits into the larger codebase
|
||||
|
||||
- **Integration with Doctor Detection**: The `@/cmd/bd/doctor.go` command runs checks to identify workspace problems, then calls functions from this package when `--fix` flag is used. The doctor command is the orchestrator that determines which issues exist and which fixes to apply.
|
||||
|
||||
- **Dependency on Core Libraries**: The fix functions use core libraries like `@/internal/deletions` (for reading/writing deletion manifests), `@/internal/types` (for issue data structures), and git operations via `exec.Command`.
|
||||
|
||||
- **Data Persistence Points**: Each fix module directly modifies persistent workspace state: deletions manifest, database files, JSONL files, and git branch configuration. Changes are written to disk and persisted in the git repository.
|
||||
|
||||
- **Deletion Tracking Architecture**: The deletions manifest (`@/internal/deletions/deletions.go`) is an append-only log tracking issue deletions. The fix in `deletions.go` is critical to maintaining the integrity of this log by preventing tombstones from being incorrectly re-added to it after `bd migrate-tombstones` runs.
|
||||
|
||||
- **Tombstone System**: The fix works in concert with the tombstone system (`@/internal/types/types.go` - `Status == StatusTombstone`). Tombstones represent soft-deleted issues that contain deletion metadata. The fix prevents tombstones from being confused with actively deleted issues during deletion hydration.
|
||||
|
||||
### Core Implementation
|
||||
|
||||
**Deletions Manifest Hydration** (`deletions.go`):
|
||||
|
||||
1. **HydrateDeletionsManifest()** (lines 16-96):
|
||||
- Entry point called by `bd doctor --fix` when "Deletions Manifest" issue is detected
|
||||
- Compares current JSONL IDs (read from `issues.jsonl`) against historical IDs from git history
|
||||
- Finds IDs that existed in history but are missing from current JSONL (legitimate deletions)
|
||||
- Adds these missing IDs to the deletions manifest with author "bd-doctor-hydrate"
|
||||
- Skips IDs already present in the existing deletions manifest to avoid duplicates
|
||||
|
||||
2. **getCurrentJSONLIDs()** (lines 98-135):
|
||||
- Reads current `issues.jsonl` file line-by-line as JSON
|
||||
- Parses each line to extract ID and Status fields
|
||||
- **CRITICAL FIX (bd-in7q)**: Skips issues with `Status == "tombstone"` (lines 127-131)
|
||||
- Returns a set of "currently active" issue IDs
|
||||
- Gracefully handles missing files (returns empty set) and malformed JSON lines (skips them)
|
||||
- This is where the bd-in7q fix is implemented - tombstones are not considered "currently active" and won't be flagged as deleted
|
||||
|
||||
3. **getHistoricalJSONLIDs()** (lines 137-148):
|
||||
- Delegates to `getHistoricalIDsViaDiff()` to extract all IDs ever present in JSONL from git history
|
||||
- Uses git log to find all commits that modified the JSONL file
|
||||
|
||||
4. **getHistoricalIDsViaDiff()** (lines 178-232):
|
||||
- Walks git history commit-by-commit (memory efficient)
|
||||
- For each commit touching the JSONL file, parses JSON to extract IDs
|
||||
- Uses `looksLikeIssueID()` validation to avoid false positives from JSON containing ID-like strings
|
||||
- Returns complete set of all IDs ever present in the repo history
|
||||
|
||||
5. **looksLikeIssueID()** (lines 150-176):
|
||||
- Validates that a string matches the issue ID format: `prefix-suffix`
|
||||
- Prefix must be alphanumeric with underscores, suffix must be base36 hash or number with optional dots for child issues
|
||||
- Used to filter out false positives when parsing JSON
|
||||
|
||||
**Test Coverage** (`deletions_test.go`):
|
||||
|
||||
The test file covers edge cases and validates the bd-in7q fix:
|
||||
|
||||
- **TestGetCurrentJSONLIDs_SkipsTombstones**: Core fix validation - verifies tombstones are excluded from current IDs
|
||||
- **TestGetCurrentJSONLIDs_HandlesEmptyFile**: Graceful handling of empty JSONL files
|
||||
- **TestGetCurrentJSONLIDs_HandlesMissingFile**: Graceful handling when JSONL doesn't exist
|
||||
- **TestGetCurrentJSONLIDs_SkipsInvalidJSON**: Malformed JSON lines are skipped without failing
|
||||
|
||||
### Things to Know
|
||||
|
||||
**The bd-in7q Bug and Fix**:
|
||||
|
||||
The bug occurred because `bd migrate-tombstones` converts deletion records from the legacy `deletions.jsonl` file into inline tombstone entries in `issues.jsonl`. Without the fix, the sequence would be:
|
||||
|
||||
1. User runs `bd migrate-tombstones` → creates tombstones in JSONL with `status: "tombstone"`
|
||||
2. User runs `bd sync` → triggers `bd doctor hydrate`
|
||||
3. `getCurrentJSONLIDs()` was reading ALL issues including tombstones
|
||||
4. Comparison logic sees tombstones are no longer in git history commit 0 (before migration)
|
||||
5. They're flagged as "deleted" and re-added to deletions manifest with author "bd-doctor-hydrate"
|
||||
6. Next sync applies these deletion records, marking issues as deleted in the database
|
||||
7. Result: thousands of false deletion records corrupt the manifest and database state
|
||||
|
||||
The fix simply filters out `Status == "tombstone"` issues in `getCurrentJSONLIDs()` (line 129). This ensures tombstones (which represent already-recorded deletions) never participate in deletion detection. They're semantically invisible to the deletion tracking system.
|
||||
|
||||
**Why Tombstones Exist**:
|
||||
|
||||
`@/internal/types/types.go` defines `StatusTombstone` as part of the system (bd-vw8). Tombstones are soft-deleted issues that retain all metadata (ID, DeletedBy, DeletedAt, DeleteReason) for audit trails and conflict resolution. They differ from entries in the deletions manifest, which are just an ID + deletion metadata without the original issue content.
|
||||
|
||||
**Append-Only Nature of Deletions Manifest**:
|
||||
|
||||
The deletions manifest (`@/internal/deletions/deletions.go`) is append-only. When a duplicate deletion is added, the last write wins (line 81 in deletions.go). This design assumes deletions are only recorded once, which the fix preserves by skipping tombstones.
|
||||
|
||||
**Missing File Handling**:
|
||||
|
||||
The `getCurrentJSONLIDs()` function returns an empty set when the JSONL file doesn't exist (lines 104-105). This is intentional - it allows hydration to work on repos that have never had issues.json yet. Only `getHistoricalIDsViaDiff()` will find historical IDs from git.
|
||||
|
||||
**ID Format Validation**:
|
||||
|
||||
The `looksLikeIssueID()` function validates format strictly (lines 150-176). This prevents parsing errors from embedded JSON with accidental ID-like strings. Example: if issue description contains `"id":"some-text"`, it won't be treated as an issue ID.
|
||||
|
||||
**Integration with Migrate Tombstones**:
|
||||
|
||||
The `@/cmd/bd/migrate_tombstones.go` command creates tombstones using `convertDeletionRecordToTombstone()` (lines 268-284). These tombstones have `Status == types.StatusTombstone`. The fix works because migrate-tombstones sets this status correctly (verified by `TestMigrateTombstones_TombstonesAreValid()` in migrate_tombstones_test.go).
|
||||
|
||||
**State Machine for Deleted Issues**:
|
||||
|
||||
There are now two ways an issue can be marked as deleted:
|
||||
1. **Database state**: Issue has `status = "tombstone"` in the database (from `@/internal/storage/sqlite`)
|
||||
2. **Manifest state**: Issue ID appears in `deletions.jsonl` (from `@/internal/deletions`)
|
||||
|
||||
The deletion hydration logic treats deletions manifest as the source of truth for what SHOULD be deleted, then applies those deletions to the database. The fix ensures the manifest only contains legitimate deletions, not tombstones that were migrated from the manifest.
|
||||
|
||||
Created and maintained by Nori.
|
||||
@@ -224,3 +224,69 @@ func TestConvertDeletionRecordToTombstone(t *testing.T) {
|
||||
t.Errorf("Expected empty OriginalType, got %s", tombstone.OriginalType)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMigrateTombstones_TombstonesAreValid verifies that migrated tombstones
|
||||
// have the tombstone status set, so they won't be re-added to deletions manifest (bd-in7q fix)
|
||||
func TestMigrateTombstones_TombstonesAreValid(t *testing.T) {
|
||||
// Setup: create temp .beads directory
|
||||
tmpDir := t.TempDir()
|
||||
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
||||
t.Fatalf("Failed to create .beads dir: %v", err)
|
||||
}
|
||||
|
||||
// Create deletions.jsonl with some entries
|
||||
deletionsPath := deletions.DefaultPath(beadsDir)
|
||||
deleteTime := time.Now().Add(-24 * time.Hour)
|
||||
|
||||
records := []deletions.DeletionRecord{
|
||||
{ID: "test-abc", Timestamp: deleteTime, Actor: "alice", Reason: "duplicate"},
|
||||
}
|
||||
|
||||
for _, record := range records {
|
||||
if err := deletions.AppendDeletion(deletionsPath, record); err != nil {
|
||||
t.Fatalf("Failed to write deletion: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create empty issues.jsonl
|
||||
issuesPath := filepath.Join(beadsDir, "issues.jsonl")
|
||||
if err := os.WriteFile(issuesPath, []byte{}, 0600); err != nil {
|
||||
t.Fatalf("Failed to create issues.jsonl: %v", err)
|
||||
}
|
||||
|
||||
// Load deletions
|
||||
loadResult, err := deletions.LoadDeletions(deletionsPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadDeletions failed: %v", err)
|
||||
}
|
||||
|
||||
// Convert to tombstones (simulating what migrate-tombstones does)
|
||||
var tombstones []*types.Issue
|
||||
for _, record := range loadResult.Records {
|
||||
ts := convertDeletionRecordToTombstone(record)
|
||||
// CRITICAL: Tombstones must have status "tombstone"
|
||||
// so they won't be re-added to deletions manifest on next sync (bd-in7q)
|
||||
if ts.Status != types.StatusTombstone {
|
||||
t.Errorf("Converted tombstone must have status 'tombstone', got %s", ts.Status)
|
||||
}
|
||||
tombstones = append(tombstones, ts)
|
||||
}
|
||||
|
||||
// Verify tombstone is valid
|
||||
if len(tombstones) != 1 {
|
||||
t.Fatalf("Expected 1 tombstone, got %d", len(tombstones))
|
||||
}
|
||||
ts := tombstones[0]
|
||||
|
||||
// These fields are critical for the doctor fix to work correctly
|
||||
if ts.ID != "test-abc" {
|
||||
t.Errorf("Expected ID test-abc, got %s", ts.ID)
|
||||
}
|
||||
if ts.Status != types.StatusTombstone {
|
||||
t.Errorf("Expected status tombstone, got %s", ts.Status)
|
||||
}
|
||||
if ts.DeletedBy != "alice" {
|
||||
t.Errorf("Expected DeletedBy 'alice', got %s", ts.DeletedBy)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user