mirror of
https://github.com/gastownhall/beads.git
synced 2026-09-14 20:17:24 +08:00
9fddc56055
* feat(storage): add downstream backend registry seam (bd-7oj) Salvage the name-keyed extension contract from PR #4859 on current main while preserving the Dolt-only OSS policy. Stock builds register no alternate backends; tombstones and unknown names still fail closed. Downstream builds can register read-write/read-only opens and workspace discovery without editing shared dispatch. Co-authored-by: Julian Knutsen <julianknutsen@users.noreply.github.com> Agent-Signature: codex-gpt-5-unknown-reasoning on behalf of CI Bot * fix(storage): dispatch and fail-closed for registered backends Address review majors on the backend registry seam: - bd init rejects a registered backend (open-existing-workspace-only) instead of silently provisioning Dolt and persisting backend: dolt. - OpenBestAvailable dispatches registered backends before any Dolt path in both CGO and non-CGO builds, mirroring the CLI store factories. - Public FindDatabasePath discovers WorkspaceIsBeadsDir workspaces via the shared registry authority instead of returning no database. - Root command reuses the resolved .beads directory for hook-runner and molecule-catalog setup instead of filepath.Dir(dbPath). For a registered WorkspaceIsBeadsDir backend dbPath is the .beads directory itself, so filepath.Dir(dbPath) loaded hooks from <repo>/hooks and molecules from <repo>/molecules.jsonl instead of .beads/hooks and .beads/molecules.jsonl. Add a single WorkspaceIsBeadsDir authority, document the init-time registration invariant, and cover the paths with tests, including a root-command test proving the .beads hook and molecule directories are selected for WorkspaceIsBeadsDir backends. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: CI Bot <ci@beads.test> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
208 lines
7.9 KiB
Go
208 lines
7.9 KiB
Go
package beads_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/steveyegge/beads"
|
|
"github.com/steveyegge/beads/internal/storage"
|
|
"github.com/steveyegge/beads/internal/storage/backends"
|
|
)
|
|
|
|
var errRegistryBackendOpen = errors.New("registry backend open sentinel")
|
|
|
|
// registerWorkspaceBackend registers a fake WorkspaceIsBeadsDir backend whose
|
|
// Open/OpenReadOnly report a sentinel instead of touching a store, so tests can
|
|
// assert that discovery and open dispatch to the registry without provisioning
|
|
// Dolt. Register requires both hooks to be non-nil.
|
|
func registerWorkspaceBackend(t *testing.T, name string) {
|
|
t.Helper()
|
|
backends.Register(name, backends.Backend{
|
|
Open: func(context.Context, string) (storage.DoltStorage, error) {
|
|
return nil, errRegistryBackendOpen
|
|
},
|
|
OpenReadOnly: func(context.Context, string) (storage.DoltStorage, error) {
|
|
return nil, errRegistryBackendOpen
|
|
},
|
|
WorkspaceIsBeadsDir: true,
|
|
})
|
|
t.Cleanup(func() { backends.Deregister(name) })
|
|
}
|
|
|
|
func writeBackendMetadata(t *testing.T, backend string) string {
|
|
t.Helper()
|
|
beadsDir := filepath.Join(t.TempDir(), ".beads")
|
|
if err := os.MkdirAll(beadsDir, 0o750); err != nil {
|
|
t.Fatalf("create .beads: %v", err)
|
|
}
|
|
metadata := `{"backend":"` + backend + `"}`
|
|
if backend == "sqlite" {
|
|
// Workspaces created by the removed SQLite backend carry an explicit
|
|
// path marker; a bare backend:"sqlite" can also be stale metadata from
|
|
// the earlier SQLite era (see PR #4740). Both must hit the tombstone.
|
|
metadata = `{"backend":"sqlite","sqlite_path":"beads.db"}`
|
|
}
|
|
if err := os.WriteFile(filepath.Join(beadsDir, "metadata.json"), []byte(metadata), 0o600); err != nil {
|
|
t.Fatalf("write metadata: %v", err)
|
|
}
|
|
return beadsDir
|
|
}
|
|
|
|
func TestOpenBestAvailableRejectsSQLite(t *testing.T) {
|
|
beadsDir := writeBackendMetadata(t, "sqlite")
|
|
store, err := beads.OpenBestAvailable(context.Background(), beadsDir)
|
|
if store != nil {
|
|
_ = store.Close()
|
|
t.Fatal("removed SQLite backend returned a store")
|
|
}
|
|
if err == nil || !strings.Contains(err.Error(), "no longer supported") {
|
|
t.Fatalf("SQLite backend error = %v, want rollback explanation", err)
|
|
}
|
|
if !strings.Contains(err.Error(), "single engine") || !strings.Contains(err.Error(), "export") {
|
|
t.Fatalf("SQLite backend error lacks rationale or migration guidance: %v", err)
|
|
}
|
|
// The fail-closed guarantee includes never provisioning the SQLite file the
|
|
// removed backend would have created.
|
|
for _, name := range []string{"embeddeddolt", "dolt", "beads.db"} {
|
|
if _, statErr := os.Stat(filepath.Join(beadsDir, name)); !os.IsNotExist(statErr) {
|
|
t.Fatalf("removed SQLite backend created %s (stat error: %v)", name, statErr)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOpenBestAvailableRejectsRemovedBackends(t *testing.T) {
|
|
for _, backend := range []string{"postgres", "mysql", "sqlite"} {
|
|
t.Run(backend, func(t *testing.T) {
|
|
beadsDir := writeBackendMetadata(t, backend)
|
|
store, err := beads.OpenBestAvailable(context.Background(), beadsDir)
|
|
if store != nil {
|
|
_ = store.Close()
|
|
t.Fatalf("removed backend %q returned a store", backend)
|
|
}
|
|
if err == nil || !strings.Contains(err.Error(), "no longer supported") {
|
|
t.Fatalf("removed backend error = %v, want rollback explanation", err)
|
|
}
|
|
rationale := "resource-light"
|
|
if backend == "sqlite" {
|
|
rationale = "single engine"
|
|
}
|
|
if !strings.Contains(err.Error(), rationale) || !strings.Contains(err.Error(), "export") {
|
|
t.Fatalf("removed backend error lacks rationale or migration guidance: %v", err)
|
|
}
|
|
for _, name := range []string{"embeddeddolt", "dolt", "beads.db"} {
|
|
if _, statErr := os.Stat(filepath.Join(beadsDir, name)); !os.IsNotExist(statErr) {
|
|
t.Fatalf("removed backend created %s (stat error: %v)", name, statErr)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOpenBestAvailableRejectsUnknownBackend(t *testing.T) {
|
|
beadsDir := writeBackendMetadata(t, "mystery")
|
|
store, err := beads.OpenBestAvailable(context.Background(), beadsDir)
|
|
if store != nil {
|
|
_ = store.Close()
|
|
t.Fatal("unknown backend returned a store")
|
|
}
|
|
if err == nil || !strings.Contains(err.Error(), "not recognized") {
|
|
t.Fatalf("unknown backend error = %v, want fail-closed metadata guidance", err)
|
|
}
|
|
if !strings.Contains(err.Error(), "no storage database was opened or modified") {
|
|
t.Fatalf("unknown backend error lacks data-safety guarantee: %v", err)
|
|
}
|
|
for _, name := range []string{"embeddeddolt", "dolt", "beads.db"} {
|
|
if _, statErr := os.Stat(filepath.Join(beadsDir, name)); !os.IsNotExist(statErr) {
|
|
t.Fatalf("unknown backend created %s (stat error: %v)", name, statErr)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOpenBestAvailableRejectsCorruptMetadata(t *testing.T) {
|
|
beadsDir := filepath.Join(t.TempDir(), ".beads")
|
|
if err := os.MkdirAll(beadsDir, 0o750); err != nil {
|
|
t.Fatalf("create .beads: %v", err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(beadsDir, "metadata.json"), []byte("{"), 0o600); err != nil {
|
|
t.Fatalf("write metadata: %v", err)
|
|
}
|
|
|
|
store, err := beads.OpenBestAvailable(context.Background(), beadsDir)
|
|
if store != nil {
|
|
_ = store.Close()
|
|
t.Fatal("corrupt metadata unexpectedly returned a store")
|
|
}
|
|
if err == nil || !strings.Contains(err.Error(), "metadata") {
|
|
t.Fatalf("corrupt metadata error = %v, want metadata load failure", err)
|
|
}
|
|
if _, statErr := os.Stat(filepath.Join(beadsDir, "embeddeddolt")); !os.IsNotExist(statErr) {
|
|
t.Fatalf("corrupt metadata created embedded Dolt storage (stat error: %v)", statErr)
|
|
}
|
|
}
|
|
|
|
// TestOpenBestAvailableDispatchesRegisteredBackend covers the public library
|
|
// open path for a registered extension backend: OpenBestAvailable must call the
|
|
// backend rather than opening Dolt (CGO) or returning the embedded-Dolt error
|
|
// (non-CGO), mirroring the CLI store factories.
|
|
func TestOpenBestAvailableDispatchesRegisteredBackend(t *testing.T) {
|
|
const name = "registry-open"
|
|
registerWorkspaceBackend(t, name)
|
|
|
|
beadsDir := writeBackendMetadata(t, name)
|
|
store, err := beads.OpenBestAvailable(context.Background(), beadsDir)
|
|
if store != nil {
|
|
_ = store.Close()
|
|
t.Fatal("registered backend dispatch returned a store instead of the backend's own result")
|
|
}
|
|
if !errors.Is(err, errRegistryBackendOpen) {
|
|
t.Fatalf("OpenBestAvailable error = %v, want registered backend Open result", err)
|
|
}
|
|
// Dispatch must not fall through and provision an embedded Dolt store.
|
|
for _, artifact := range []string{"embeddeddolt", "dolt"} {
|
|
if _, statErr := os.Stat(filepath.Join(beadsDir, artifact)); !os.IsNotExist(statErr) {
|
|
t.Fatalf("registered backend dispatch created %s (stat error: %v)", artifact, statErr)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestFindDatabasePathDiscoversRegisteredWorkspace covers public discovery
|
|
// parity: a registered WorkspaceIsBeadsDir backend has no local Dolt database,
|
|
// so FindDatabasePath must return the .beads directory itself instead of the
|
|
// empty "no database" result the Dolt-only search would give.
|
|
func TestFindDatabasePathDiscoversRegisteredWorkspace(t *testing.T) {
|
|
const name = "registry-discovery"
|
|
registerWorkspaceBackend(t, name)
|
|
|
|
beadsDir := writeBackendMetadata(t, name)
|
|
t.Setenv("BEADS_DIR", beadsDir)
|
|
|
|
got := beads.FindDatabasePath()
|
|
if got == "" {
|
|
t.Fatal("FindDatabasePath returned empty for a WorkspaceIsBeadsDir backend")
|
|
}
|
|
// Path canonicalization (symlinked temp dirs) can rewrite the string, so
|
|
// compare the workspace by identity rather than raw path equality.
|
|
gotInfo, err := os.Stat(got)
|
|
if err != nil {
|
|
t.Fatalf("stat discovered path %q: %v", got, err)
|
|
}
|
|
wantInfo, err := os.Stat(beadsDir)
|
|
if err != nil {
|
|
t.Fatalf("stat beads dir %q: %v", beadsDir, err)
|
|
}
|
|
if !os.SameFile(gotInfo, wantInfo) {
|
|
t.Fatalf("FindDatabasePath = %q, want the .beads workspace dir %q", got, beadsDir)
|
|
}
|
|
// The registry-only workspace carries no local Dolt database and discovery
|
|
// must not create one.
|
|
for _, artifact := range []string{"embeddeddolt", "dolt"} {
|
|
if _, statErr := os.Stat(filepath.Join(beadsDir, artifact)); !os.IsNotExist(statErr) {
|
|
t.Fatalf("registry workspace discovery created %s (stat error: %v)", artifact, statErr)
|
|
}
|
|
}
|
|
}
|