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>
347 lines
9.9 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|