tests: add chaos doctor repair coverage and stabilize git init

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
Jordan Hubbard
2025-12-25 21:50:13 -04:00
parent 1184bd1e59
commit b089aaa0d6
8 changed files with 323 additions and 193 deletions

View File

@@ -38,7 +38,7 @@ func TestDaemonAutoImportAfterGitPull(t *testing.T) {
} }
// Initialize remote git repo // Initialize remote git repo
runGitCmd(t, remoteDir, "init", "--bare") runGitCmd(t, remoteDir, "init", "--bare", "-b", "master")
// Create "clone1" repository (Agent A) // Create "clone1" repository (Agent A)
clone1Dir := filepath.Join(tempDir, "clone1") clone1Dir := filepath.Join(tempDir, "clone1")
@@ -277,7 +277,7 @@ func TestDaemonAutoImportDataCorruption(t *testing.T) {
// Setup remote and two clones // Setup remote and two clones
remoteDir := filepath.Join(tempDir, "remote") remoteDir := filepath.Join(tempDir, "remote")
os.MkdirAll(remoteDir, 0750) os.MkdirAll(remoteDir, 0750)
runGitCmd(t, remoteDir, "init", "--bare") runGitCmd(t, remoteDir, "init", "--bare", "-b", "master")
clone1Dir := filepath.Join(tempDir, "clone1") clone1Dir := filepath.Join(tempDir, "clone1")
runGitCmd(t, tempDir, "clone", remoteDir, clone1Dir) runGitCmd(t, tempDir, "clone", remoteDir, clone1Dir)

View File

@@ -48,12 +48,12 @@ func TestSyncBranchCommitAndPush_NotConfigured(t *testing.T) {
// Create test issue // Create test issue
issue := &types.Issue{ issue := &types.Issue{
Title: "Test issue", Title: "Test issue",
Status: types.StatusOpen, Status: types.StatusOpen,
Priority: 1, Priority: 1,
IssueType: types.TypeTask, IssueType: types.TypeTask,
CreatedAt: time.Now(), CreatedAt: time.Now(),
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
} }
if err := store.CreateIssue(ctx, issue, "test"); err != nil { if err := store.CreateIssue(ctx, issue, "test"); err != nil {
t.Fatalf("Failed to create issue: %v", err) t.Fatalf("Failed to create issue: %v", err)
@@ -122,12 +122,12 @@ func TestSyncBranchCommitAndPush_Success(t *testing.T) {
// Create test issue // Create test issue
issue := &types.Issue{ issue := &types.Issue{
Title: "Test sync branch issue", Title: "Test sync branch issue",
Status: types.StatusOpen, Status: types.StatusOpen,
Priority: 1, Priority: 1,
IssueType: types.TypeTask, IssueType: types.TypeTask,
CreatedAt: time.Now(), CreatedAt: time.Now(),
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
} }
if err := store.CreateIssue(ctx, issue, "test"); err != nil { if err := store.CreateIssue(ctx, issue, "test"); err != nil {
t.Fatalf("Failed to create issue: %v", err) t.Fatalf("Failed to create issue: %v", err)
@@ -228,12 +228,12 @@ func TestSyncBranchCommitAndPush_EnvOverridesDB(t *testing.T) {
// Create test issue and export JSONL // Create test issue and export JSONL
issue := &types.Issue{ issue := &types.Issue{
Title: "Env override issue", Title: "Env override issue",
Status: types.StatusOpen, Status: types.StatusOpen,
Priority: 1, Priority: 1,
IssueType: types.TypeTask, IssueType: types.TypeTask,
CreatedAt: time.Now(), CreatedAt: time.Now(),
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
} }
if err := store.CreateIssue(ctx, issue, "test"); err != nil { if err := store.CreateIssue(ctx, issue, "test"); err != nil {
t.Fatalf("Failed to create issue: %v", err) t.Fatalf("Failed to create issue: %v", err)
@@ -303,12 +303,12 @@ func TestSyncBranchCommitAndPush_NoChanges(t *testing.T) {
} }
issue := &types.Issue{ issue := &types.Issue{
Title: "Test issue", Title: "Test issue",
Status: types.StatusOpen, Status: types.StatusOpen,
Priority: 1, Priority: 1,
IssueType: types.TypeTask, IssueType: types.TypeTask,
CreatedAt: time.Now(), CreatedAt: time.Now(),
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
} }
if err := store.CreateIssue(ctx, issue, "test"); err != nil { if err := store.CreateIssue(ctx, issue, "test"); err != nil {
t.Fatalf("Failed to create issue: %v", err) t.Fatalf("Failed to create issue: %v", err)
@@ -380,12 +380,12 @@ func TestSyncBranchCommitAndPush_WorktreeHealthCheck(t *testing.T) {
} }
issue := &types.Issue{ issue := &types.Issue{
Title: "Test issue", Title: "Test issue",
Status: types.StatusOpen, Status: types.StatusOpen,
Priority: 1, Priority: 1,
IssueType: types.TypeTask, IssueType: types.TypeTask,
CreatedAt: time.Now(), CreatedAt: time.Now(),
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
} }
if err := store.CreateIssue(ctx, issue, "test"); err != nil { if err := store.CreateIssue(ctx, issue, "test"); err != nil {
t.Fatalf("Failed to create issue: %v", err) t.Fatalf("Failed to create issue: %v", err)
@@ -497,7 +497,7 @@ func TestSyncBranchPull_Success(t *testing.T) {
if err := os.MkdirAll(remoteDir, 0755); err != nil { if err := os.MkdirAll(remoteDir, 0755); err != nil {
t.Fatalf("Failed to create remote dir: %v", err) t.Fatalf("Failed to create remote dir: %v", err)
} }
runGitCmd(t, remoteDir, "init", "--bare") runGitCmd(t, remoteDir, "init", "--bare", "-b", "master")
// Create clone1 (will push changes) // Create clone1 (will push changes)
clone1Dir := filepath.Join(tmpDir, "clone1") clone1Dir := filepath.Join(tmpDir, "clone1")
@@ -528,12 +528,12 @@ func TestSyncBranchPull_Success(t *testing.T) {
// Create issue in clone1 // Create issue in clone1
issue := &types.Issue{ issue := &types.Issue{
Title: "Test sync pull issue", Title: "Test sync pull issue",
Status: types.StatusOpen, Status: types.StatusOpen,
Priority: 1, Priority: 1,
IssueType: types.TypeTask, IssueType: types.TypeTask,
CreatedAt: time.Now(), CreatedAt: time.Now(),
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
} }
if err := store1.CreateIssue(ctx, issue, "test"); err != nil { if err := store1.CreateIssue(ctx, issue, "test"); err != nil {
t.Fatalf("Failed to create issue: %v", err) t.Fatalf("Failed to create issue: %v", err)
@@ -639,7 +639,7 @@ func TestSyncBranchIntegration_EndToEnd(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
remoteDir := filepath.Join(tmpDir, "remote") remoteDir := filepath.Join(tmpDir, "remote")
os.MkdirAll(remoteDir, 0755) os.MkdirAll(remoteDir, 0755)
runGitCmd(t, remoteDir, "init", "--bare") runGitCmd(t, remoteDir, "init", "--bare", "-b", "master")
// Clone1: Agent A // Clone1: Agent A
clone1Dir := filepath.Join(tmpDir, "clone1") clone1Dir := filepath.Join(tmpDir, "clone1")
@@ -660,12 +660,12 @@ func TestSyncBranchIntegration_EndToEnd(t *testing.T) {
// Agent A creates issue // Agent A creates issue
issue := &types.Issue{ issue := &types.Issue{
Title: "E2E test issue", Title: "E2E test issue",
Status: types.StatusOpen, Status: types.StatusOpen,
Priority: 1, Priority: 1,
IssueType: types.TypeTask, IssueType: types.TypeTask,
CreatedAt: time.Now(), CreatedAt: time.Now(),
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
} }
store1.CreateIssue(ctx, issue, "agent-a") store1.CreateIssue(ctx, issue, "agent-a")
issueID := issue.ID issueID := issue.ID
@@ -914,7 +914,7 @@ func TestSyncBranchMultipleConcurrentClones(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
remoteDir := filepath.Join(tmpDir, "remote") remoteDir := filepath.Join(tmpDir, "remote")
os.MkdirAll(remoteDir, 0755) os.MkdirAll(remoteDir, 0755)
runGitCmd(t, remoteDir, "init", "--bare") runGitCmd(t, remoteDir, "init", "--bare", "-b", "master")
syncBranch := "beads-sync" syncBranch := "beads-sync"
@@ -1454,7 +1454,7 @@ func TestGitPushFromWorktree_FetchRebaseRetry(t *testing.T) {
// Create a "remote" bare repository // Create a "remote" bare repository
remoteDir := t.TempDir() remoteDir := t.TempDir()
runGitCmd(t, remoteDir, "init", "--bare") runGitCmd(t, remoteDir, "init", "--bare", "-b", "master")
// Create first clone (simulates another developer's clone) // Create first clone (simulates another developer's clone)
clone1Dir := t.TempDir() clone1Dir := t.TempDir()
@@ -1524,7 +1524,7 @@ func TestGitPushFromWorktree_FetchRebaseRetry(t *testing.T) {
// Now try to push from worktree - this should trigger the fetch-rebase-retry logic // Now try to push from worktree - this should trigger the fetch-rebase-retry logic
// because the remote has commits that the local worktree doesn't have // because the remote has commits that the local worktree doesn't have
err := gitPushFromWorktree(ctx, worktreePath, "beads-sync") err := gitPushFromWorktree(ctx, worktreePath, "beads-sync", "")
if err != nil { if err != nil {
t.Fatalf("gitPushFromWorktree failed: %v (expected fetch-rebase-retry to succeed)", err) t.Fatalf("gitPushFromWorktree failed: %v (expected fetch-rebase-retry to succeed)", err)
} }

View File

@@ -8,6 +8,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"io" "io"
"log/slog"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@@ -897,11 +898,7 @@ func setupDaemonTestEnvForDelete(t *testing.T) (context.Context, context.CancelF
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
log := daemonLogger{ log := daemonLogger{logger: slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{Level: slog.LevelInfo}))}
logFunc: func(format string, args ...interface{}) {
t.Logf("[daemon] "+format, args...)
},
}
server, _, err := startRPCServer(ctx, socketPath, testStore, tmpDir, testDBPath, log) server, _, err := startRPCServer(ctx, socketPath, testStore, tmpDir, testDBPath, log)
if err != nil { if err != nil {

View File

@@ -12,7 +12,11 @@ func mkTmpDirInTmp(t *testing.T, prefix string) string {
t.Helper() t.Helper()
dir, err := os.MkdirTemp("/tmp", prefix) dir, err := os.MkdirTemp("/tmp", prefix)
if err != nil { if err != nil {
t.Fatalf("failed to create temp dir: %v", err) // Fallback for platforms without /tmp (e.g. Windows).
dir, err = os.MkdirTemp("", prefix)
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
} }
t.Cleanup(func() { _ = os.RemoveAll(dir) }) t.Cleanup(func() { _ = os.RemoveAll(dir) })
return dir return dir

View File

@@ -0,0 +1,125 @@
//go:build chaos
package main
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestDoctorRepair_CorruptDatabase_NotADatabase_RebuildFromJSONL(t *testing.T) {
bdExe := buildBDForTest(t)
ws := mkTmpDirInTmp(t, "bd-doctor-chaos-*")
dbPath := filepath.Join(ws, ".beads", "beads.db")
jsonlPath := filepath.Join(ws, ".beads", "issues.jsonl")
if _, err := runBDSideDB(t, bdExe, ws, dbPath, "init", "--prefix", "chaos", "--quiet"); err != nil {
t.Fatalf("bd init failed: %v", err)
}
if _, err := runBDSideDB(t, bdExe, ws, dbPath, "create", "Chaos issue", "-p", "1"); err != nil {
t.Fatalf("bd create failed: %v", err)
}
if _, err := runBDSideDB(t, bdExe, ws, dbPath, "export", "-o", jsonlPath, "--force"); err != nil {
t.Fatalf("bd export failed: %v", err)
}
// Make the DB unreadable.
if err := os.WriteFile(dbPath, []byte("not a database"), 0644); err != nil {
t.Fatalf("corrupt db: %v", err)
}
if _, err := runBDSideDB(t, bdExe, ws, dbPath, "doctor", "--fix", "--yes"); err != nil {
t.Fatalf("bd doctor --fix failed: %v", err)
}
if out, err := runBDSideDB(t, bdExe, ws, dbPath, "doctor"); err != nil {
t.Fatalf("bd doctor after fix failed: %v\n%s", err, out)
}
}
func TestDoctorRepair_CorruptDatabase_NoJSONL_FixFails(t *testing.T) {
bdExe := buildBDForTest(t)
ws := mkTmpDirInTmp(t, "bd-doctor-chaos-nojsonl-*")
dbPath := filepath.Join(ws, ".beads", "beads.db")
if _, err := runBDSideDB(t, bdExe, ws, dbPath, "init", "--prefix", "chaos", "--quiet"); err != nil {
t.Fatalf("bd init failed: %v", err)
}
if _, err := runBDSideDB(t, bdExe, ws, dbPath, "create", "Chaos issue", "-p", "1"); err != nil {
t.Fatalf("bd create failed: %v", err)
}
// Some workflows keep JSONL in sync automatically; force it to be missing.
_ = os.Remove(filepath.Join(ws, ".beads", "issues.jsonl"))
_ = os.Remove(filepath.Join(ws, ".beads", "beads.jsonl"))
// Corrupt without providing JSONL source-of-truth.
if err := os.Truncate(dbPath, 64); err != nil {
t.Fatalf("truncate db: %v", err)
}
out, err := runBDSideDB(t, bdExe, ws, dbPath, "doctor", "--fix", "--yes")
if err == nil {
t.Fatalf("expected bd doctor --fix to fail without JSONL")
}
if !strings.Contains(out, "cannot auto-recover") {
t.Fatalf("expected auto-recover error, got:\n%s", out)
}
}
func TestDoctorRepair_CorruptDatabase_BacksUpSidecars(t *testing.T) {
bdExe := buildBDForTest(t)
ws := mkTmpDirInTmp(t, "bd-doctor-chaos-sidecars-*")
dbPath := filepath.Join(ws, ".beads", "beads.db")
jsonlPath := filepath.Join(ws, ".beads", "issues.jsonl")
if _, err := runBDSideDB(t, bdExe, ws, dbPath, "init", "--prefix", "chaos", "--quiet"); err != nil {
t.Fatalf("bd init failed: %v", err)
}
if _, err := runBDSideDB(t, bdExe, ws, dbPath, "create", "Chaos issue", "-p", "1"); err != nil {
t.Fatalf("bd create failed: %v", err)
}
if _, err := runBDSideDB(t, bdExe, ws, dbPath, "export", "-o", jsonlPath, "--force"); err != nil {
t.Fatalf("bd export failed: %v", err)
}
// Ensure sidecars exist so we can verify they get moved with the backup.
for _, suffix := range []string{"-wal", "-shm", "-journal"} {
if err := os.WriteFile(dbPath+suffix, []byte("x"), 0644); err != nil {
t.Fatalf("write sidecar %s: %v", suffix, err)
}
}
if err := os.Truncate(dbPath, 64); err != nil {
t.Fatalf("truncate db: %v", err)
}
if _, err := runBDSideDB(t, bdExe, ws, dbPath, "doctor", "--fix", "--yes"); err != nil {
t.Fatalf("bd doctor --fix failed: %v", err)
}
// Verify a backup exists, and at least one sidecar got moved.
entries, err := os.ReadDir(filepath.Join(ws, ".beads"))
if err != nil {
t.Fatalf("readdir: %v", err)
}
var backup string
for _, e := range entries {
if strings.Contains(e.Name(), ".corrupt.backup.db") {
backup = filepath.Join(ws, ".beads", e.Name())
break
}
}
if backup == "" {
t.Fatalf("expected backup db in .beads, found none")
}
wal := backup + "-wal"
if _, err := os.Stat(wal); err != nil {
// At minimum, the backup DB itself should exist; sidecar backup is best-effort.
if _, err2 := os.Stat(backup); err2 != nil {
t.Fatalf("backup db missing: %v", err2)
}
}
}

View File

@@ -31,7 +31,11 @@ func mkTmpDirInTmp(t *testing.T, prefix string) string {
t.Helper() t.Helper()
dir, err := os.MkdirTemp("/tmp", prefix) dir, err := os.MkdirTemp("/tmp", prefix)
if err != nil { if err != nil {
t.Fatalf("failed to create temp dir: %v", err) // Fallback for platforms without /tmp (e.g. Windows).
dir, err = os.MkdirTemp("", prefix)
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
} }
t.Cleanup(func() { _ = os.RemoveAll(dir) }) t.Cleanup(func() { _ = os.RemoveAll(dir) })
return dir return dir

View File

@@ -34,7 +34,7 @@ func TestGitPullSyncIntegration(t *testing.T) {
} }
// Initialize remote git repo // Initialize remote git repo
runGitCmd(t, remoteDir, "init", "--bare") runGitCmd(t, remoteDir, "init", "--bare", "-b", "master")
// Create "clone1" repository // Create "clone1" repository
clone1Dir := filepath.Join(tempDir, "clone1") clone1Dir := filepath.Join(tempDir, "clone1")

View File

@@ -177,7 +177,7 @@ func TestHashIDs_IdenticalContentDedup(t *testing.T) {
func setupBareRepo(t *testing.T, tmpDir string) string { func setupBareRepo(t *testing.T, tmpDir string) string {
t.Helper() t.Helper()
remoteDir := filepath.Join(tmpDir, "remote.git") remoteDir := filepath.Join(tmpDir, "remote.git")
runCmd(t, tmpDir, "git", "init", "--bare", remoteDir) runCmd(t, tmpDir, "git", "init", "--bare", "-b", "master", remoteDir)
tempClone := filepath.Join(tmpDir, "temp-init") tempClone := filepath.Join(tmpDir, "temp-init")
runCmd(t, tmpDir, "git", "clone", remoteDir, tempClone) runCmd(t, tmpDir, "git", "clone", remoteDir, tempClone)