Files
beads/internal/storage/dolt/versioned_test.go
beads/crew/lizzy 938a17cda5 feat(dolt): add GetChangesSinceExport and CommitExists methods (bd-ejv83)
Add versioned storage methods for incremental export support:
- GetChangesSinceExport: returns changes since a commit hash, with
  NeedsFullExport flag for invalid/GC'd commits
- CommitExists: checks if a commit hash exists, supports short prefixes

Also fixes dolt_diff syntax for embedded driver (from_ref, to_ref, table).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 20:47:48 -08:00

347 lines
9.9 KiB
Go

package dolt
import (
"testing"
"github.com/steveyegge/beads/internal/storage"
"github.com/steveyegge/beads/internal/types"
)
// TestDoltStoreImplementsVersionedStorage verifies DoltStore implements VersionedStorage.
// This is a compile-time check.
func TestDoltStoreImplementsVersionedStorage(t *testing.T) {
// The var _ declaration in versioned.go already ensures this at compile time.
// This test just documents the expectation.
var _ storage.VersionedStorage = (*DoltStore)(nil)
}
// TestVersionedStorageMethodsExist ensures all required methods are defined.
// This is mostly a documentation test since Go's type system enforces this.
func TestVersionedStorageMethodsExist(t *testing.T) {
// If DoltStore doesn't implement all VersionedStorage methods,
// this file won't compile. This test exists for documentation.
t.Log("DoltStore implements all VersionedStorage methods")
}
// TestCommitExists tests the CommitExists method.
func TestCommitExists(t *testing.T) {
store, cleanup := setupTestStore(t)
defer cleanup()
ctx, cancel := testContext(t)
defer cancel()
// Get the current commit hash (should exist after store initialization)
currentCommit, err := store.GetCurrentCommit(ctx)
if err != nil {
t.Fatalf("failed to get current commit: %v", err)
}
t.Run("valid commit hash returns true", func(t *testing.T) {
exists, err := store.CommitExists(ctx, currentCommit)
if err != nil {
t.Fatalf("CommitExists failed: %v", err)
}
if !exists {
t.Errorf("expected commit %s to exist", currentCommit)
}
})
t.Run("short hash prefix returns true", func(t *testing.T) {
// Use first 8 characters as a short hash (like git's default short SHA)
if len(currentCommit) < 8 {
t.Skip("commit hash too short for prefix test")
}
shortHash := currentCommit[:8]
exists, err := store.CommitExists(ctx, shortHash)
if err != nil {
t.Fatalf("CommitExists failed: %v", err)
}
if !exists {
t.Errorf("expected short hash %s to match commit %s", shortHash, currentCommit)
}
})
t.Run("invalid nonexistent commit returns false", func(t *testing.T) {
exists, err := store.CommitExists(ctx, "0000000000000000000000000000000000000000")
if err != nil {
t.Fatalf("CommitExists failed: %v", err)
}
if exists {
t.Error("expected nonexistent commit to return false")
}
})
t.Run("empty string returns false", func(t *testing.T) {
exists, err := store.CommitExists(ctx, "")
if err != nil {
t.Fatalf("CommitExists failed: %v", err)
}
if exists {
t.Error("expected empty string to return false")
}
})
t.Run("malformed input returns false", func(t *testing.T) {
testCases := []string{
"invalid hash with spaces",
"hash'with'quotes",
"hash;injection",
"hash--comment",
}
for _, tc := range testCases {
exists, err := store.CommitExists(ctx, tc)
if err != nil {
t.Fatalf("CommitExists(%q) returned error: %v", tc, err)
}
if exists {
t.Errorf("expected malformed input %q to return false", tc)
}
}
})
}
// TestGetChangesSinceExport tests the GetChangesSinceExport method.
func TestGetChangesSinceExport(t *testing.T) {
store, cleanup := setupTestStore(t)
defer cleanup()
ctx, cancel := testContext(t)
defer cancel()
t.Run("empty commit returns needsFullExport", func(t *testing.T) {
result, err := store.GetChangesSinceExport(ctx, "")
if err != nil {
t.Fatalf("GetChangesSinceExport failed: %v", err)
}
if !result.NeedsFullExport {
t.Error("expected NeedsFullExport=true for empty commit")
}
})
t.Run("invalid commit returns needsFullExport", func(t *testing.T) {
result, err := store.GetChangesSinceExport(ctx, "nonexistent123456789012345678901234567890")
if err != nil {
t.Fatalf("GetChangesSinceExport failed: %v", err)
}
if !result.NeedsFullExport {
t.Error("expected NeedsFullExport=true for invalid commit")
}
})
t.Run("malformed commit returns needsFullExport", func(t *testing.T) {
result, err := store.GetChangesSinceExport(ctx, "invalid'hash")
if err != nil {
t.Fatalf("GetChangesSinceExport failed: %v", err)
}
if !result.NeedsFullExport {
t.Error("expected NeedsFullExport=true for malformed commit")
}
})
t.Run("no changes returns empty entries", func(t *testing.T) {
// First create and commit an issue so the issues table has committed data
issue := &types.Issue{
ID: "test-export-baseline",
Title: "Baseline Issue",
Description: "Ensures table exists in committed state",
Status: types.StatusOpen,
Priority: 2,
IssueType: types.TypeTask,
}
if err := store.CreateIssue(ctx, issue, "tester"); err != nil {
t.Fatalf("failed to create baseline issue: %v", err)
}
if err := store.Commit(ctx, "Add baseline issue"); err != nil {
t.Fatalf("failed to commit baseline: %v", err)
}
// Get current commit
currentCommit, err := store.GetCurrentCommit(ctx)
if err != nil {
t.Fatalf("failed to get current commit: %v", err)
}
// Query changes since current commit (should be none)
result, err := store.GetChangesSinceExport(ctx, currentCommit)
if err != nil {
t.Fatalf("GetChangesSinceExport failed: %v", err)
}
if result.NeedsFullExport {
t.Error("expected NeedsFullExport=false for valid commit")
}
if len(result.Entries) != 0 {
t.Errorf("expected 0 entries, got %d", len(result.Entries))
}
})
t.Run("create issue shows added in diff", func(t *testing.T) {
// Get commit before creating issue
beforeCommit, err := store.GetCurrentCommit(ctx)
if err != nil {
t.Fatalf("failed to get current commit: %v", err)
}
// Create an issue
issue := &types.Issue{
ID: "test-export-add",
Title: "Test Export Add",
Description: "Testing added detection",
Status: types.StatusOpen,
Priority: 2,
IssueType: types.TypeTask,
}
if err := store.CreateIssue(ctx, issue, "tester"); err != nil {
t.Fatalf("failed to create issue: %v", err)
}
// Commit the changes
if err := store.Commit(ctx, "Add test issue"); err != nil {
t.Fatalf("failed to commit: %v", err)
}
// Get changes since before
result, err := store.GetChangesSinceExport(ctx, beforeCommit)
if err != nil {
t.Fatalf("GetChangesSinceExport failed: %v", err)
}
if result.NeedsFullExport {
t.Error("expected NeedsFullExport=false")
}
// Find the added entry
var foundAdded bool
for _, entry := range result.Entries {
if entry.IssueID == issue.ID && entry.DiffType == "added" {
foundAdded = true
if entry.NewValue == nil {
t.Error("expected NewValue to be set for added entry")
}
break
}
}
if !foundAdded {
t.Errorf("expected to find 'added' entry for issue %s", issue.ID)
}
})
t.Run("update issue shows modified in diff", func(t *testing.T) {
// Create an issue first
issue := &types.Issue{
ID: "test-export-modify",
Title: "Test Export Modify",
Description: "Testing modified detection",
Status: types.StatusOpen,
Priority: 2,
IssueType: types.TypeTask,
}
if err := store.CreateIssue(ctx, issue, "tester"); err != nil {
t.Fatalf("failed to create issue: %v", err)
}
if err := store.Commit(ctx, "Add issue for modify test"); err != nil {
t.Fatalf("failed to commit: %v", err)
}
// Get commit before updating
beforeCommit, err := store.GetCurrentCommit(ctx)
if err != nil {
t.Fatalf("failed to get current commit: %v", err)
}
// Update the issue
updates := map[string]interface{}{
"title": "Updated Title",
}
if err := store.UpdateIssue(ctx, issue.ID, updates, "tester"); err != nil {
t.Fatalf("failed to update issue: %v", err)
}
if err := store.Commit(ctx, "Update test issue"); err != nil {
t.Fatalf("failed to commit: %v", err)
}
// Get changes since before
result, err := store.GetChangesSinceExport(ctx, beforeCommit)
if err != nil {
t.Fatalf("GetChangesSinceExport failed: %v", err)
}
if result.NeedsFullExport {
t.Error("expected NeedsFullExport=false")
}
// Find the modified entry
var foundModified bool
for _, entry := range result.Entries {
if entry.IssueID == issue.ID && entry.DiffType == "modified" {
foundModified = true
if entry.OldValue == nil || entry.NewValue == nil {
t.Error("expected both OldValue and NewValue to be set for modified entry")
}
break
}
}
if !foundModified {
t.Errorf("expected to find 'modified' entry for issue %s", issue.ID)
}
})
t.Run("delete issue shows removed in diff", func(t *testing.T) {
// Create an issue first
issue := &types.Issue{
ID: "test-export-delete",
Title: "Test Export Delete",
Description: "Testing removed detection",
Status: types.StatusOpen,
Priority: 2,
IssueType: types.TypeTask,
}
if err := store.CreateIssue(ctx, issue, "tester"); err != nil {
t.Fatalf("failed to create issue: %v", err)
}
if err := store.Commit(ctx, "Add issue for delete test"); err != nil {
t.Fatalf("failed to commit: %v", err)
}
// Get commit before deleting
beforeCommit, err := store.GetCurrentCommit(ctx)
if err != nil {
t.Fatalf("failed to get current commit: %v", err)
}
// Delete the issue
if err := store.DeleteIssue(ctx, issue.ID); err != nil {
t.Fatalf("failed to delete issue: %v", err)
}
if err := store.Commit(ctx, "Delete test issue"); err != nil {
t.Fatalf("failed to commit: %v", err)
}
// Get changes since before
result, err := store.GetChangesSinceExport(ctx, beforeCommit)
if err != nil {
t.Fatalf("GetChangesSinceExport failed: %v", err)
}
if result.NeedsFullExport {
t.Error("expected NeedsFullExport=false")
}
// Find the removed entry
var foundRemoved bool
for _, entry := range result.Entries {
if entry.IssueID == issue.ID && entry.DiffType == "removed" {
foundRemoved = true
if entry.OldValue == nil {
t.Error("expected OldValue to be set for removed entry")
}
break
}
}
if !foundRemoved {
t.Errorf("expected to find 'removed' entry for issue %s", issue.ID)
}
})
}