doctor: add JSONL integrity check/fix and harden repairs
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
118
cmd/bd/test_repo_beads_guard_test.go
Normal file
118
cmd/bd/test_repo_beads_guard_test.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Guardrail: ensure the cmd/bd test suite does not touch the real repo .beads state.
|
||||
// Disable with BEADS_TEST_GUARD_DISABLE=1 (useful when running tests while actively using beads).
|
||||
func TestMain(m *testing.M) {
|
||||
if os.Getenv("BEADS_TEST_GUARD_DISABLE") != "" {
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
repoRoot := findRepoRoot()
|
||||
if repoRoot == "" {
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
repoBeadsDir := filepath.Join(repoRoot, ".beads")
|
||||
if _, err := os.Stat(repoBeadsDir); err != nil {
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
watch := []string{
|
||||
"beads.db",
|
||||
"beads.db-wal",
|
||||
"beads.db-shm",
|
||||
"beads.db-journal",
|
||||
"issues.jsonl",
|
||||
"beads.jsonl",
|
||||
"metadata.json",
|
||||
"interactions.jsonl",
|
||||
"deletions.jsonl",
|
||||
"molecules.jsonl",
|
||||
"daemon.lock",
|
||||
"daemon.pid",
|
||||
"bd.sock",
|
||||
}
|
||||
|
||||
before := snapshotFiles(repoBeadsDir, watch)
|
||||
code := m.Run()
|
||||
after := snapshotFiles(repoBeadsDir, watch)
|
||||
|
||||
if diff := diffSnapshots(before, after); diff != "" {
|
||||
fmt.Fprintf(os.Stderr, "ERROR: test suite modified repo .beads state:\n%s\n", diff)
|
||||
if code == 0 {
|
||||
code = 1
|
||||
}
|
||||
}
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
type fileSnap struct {
|
||||
exists bool
|
||||
size int64
|
||||
modUnix int64
|
||||
}
|
||||
|
||||
func snapshotFiles(dir string, names []string) map[string]fileSnap {
|
||||
out := make(map[string]fileSnap, len(names))
|
||||
for _, name := range names {
|
||||
p := filepath.Join(dir, name)
|
||||
info, err := os.Stat(p)
|
||||
if err != nil {
|
||||
out[name] = fileSnap{exists: false}
|
||||
continue
|
||||
}
|
||||
out[name] = fileSnap{exists: true, size: info.Size(), modUnix: info.ModTime().UnixNano()}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func diffSnapshots(before, after map[string]fileSnap) string {
|
||||
var out string
|
||||
for name, b := range before {
|
||||
a := after[name]
|
||||
if b.exists != a.exists {
|
||||
out += fmt.Sprintf("- %s: exists %v → %v\n", name, b.exists, a.exists)
|
||||
continue
|
||||
}
|
||||
if !b.exists {
|
||||
continue
|
||||
}
|
||||
if b.size != a.size || b.modUnix != a.modUnix {
|
||||
out += fmt.Sprintf("- %s: size %d → %d, mtime %s → %s\n",
|
||||
name,
|
||||
b.size,
|
||||
a.size,
|
||||
time.Unix(0, b.modUnix).UTC().Format(time.RFC3339Nano),
|
||||
time.Unix(0, a.modUnix).UTC().Format(time.RFC3339Nano),
|
||||
)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func findRepoRoot() string {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
for i := 0; i < 25; i++ {
|
||||
if _, err := os.Stat(filepath.Join(wd, "go.mod")); err == nil {
|
||||
return wd
|
||||
}
|
||||
parent := filepath.Dir(wd)
|
||||
if parent == wd {
|
||||
break
|
||||
}
|
||||
wd = parent
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user