- Use exec.LookPath instead of hardcoded path for Dolt detection - Add test context with timeout to prevent tests from hanging - Document known issues with embedded Dolt driver async operations The embedded Dolt driver can hang on complex JOIN queries. This change ensures tests fail gracefully with timeout rather than hanging. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
871 lines
21 KiB
Go
871 lines
21 KiB
Go
package dolt
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"os/exec"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/steveyegge/beads/internal/types"
|
|
)
|
|
|
|
// testTimeout is the maximum time for any single test operation.
|
|
// The embedded Dolt driver can be slow, especially for complex JOIN queries.
|
|
// If tests are timing out, it may indicate an issue with the embedded Dolt
|
|
// driver's async operations rather than with the DoltStore implementation.
|
|
const testTimeout = 30 * time.Second
|
|
|
|
// testContext returns a context with timeout for test operations
|
|
func testContext(t *testing.T) (context.Context, context.CancelFunc) {
|
|
t.Helper()
|
|
return context.WithTimeout(context.Background(), testTimeout)
|
|
}
|
|
|
|
// skipIfNoDolt skips the test if Dolt is not installed
|
|
func skipIfNoDolt(t *testing.T) {
|
|
t.Helper()
|
|
if _, err := exec.LookPath("dolt"); err != nil {
|
|
t.Skip("Dolt not installed, skipping test")
|
|
}
|
|
}
|
|
|
|
// setupTestStore creates a test store with a temporary directory
|
|
func setupTestStore(t *testing.T) (*DoltStore, func()) {
|
|
t.Helper()
|
|
skipIfNoDolt(t)
|
|
|
|
ctx, cancel := testContext(t)
|
|
defer cancel()
|
|
|
|
tmpDir, err := os.MkdirTemp("", "dolt-test-*")
|
|
if err != nil {
|
|
t.Fatalf("failed to create temp dir: %v", err)
|
|
}
|
|
|
|
cfg := &Config{
|
|
Path: tmpDir,
|
|
CommitterName: "test",
|
|
CommitterEmail: "test@example.com",
|
|
Database: "testdb",
|
|
}
|
|
|
|
store, err := New(ctx, cfg)
|
|
if err != nil {
|
|
os.RemoveAll(tmpDir)
|
|
t.Fatalf("failed to create Dolt store: %v", err)
|
|
}
|
|
|
|
// Set up issue prefix
|
|
if err := store.SetConfig(ctx, "issue_prefix", "test"); err != nil {
|
|
store.Close()
|
|
os.RemoveAll(tmpDir)
|
|
t.Fatalf("failed to set prefix: %v", err)
|
|
}
|
|
|
|
cleanup := func() {
|
|
store.Close()
|
|
os.RemoveAll(tmpDir)
|
|
}
|
|
|
|
return store, cleanup
|
|
}
|
|
|
|
func TestNewDoltStore(t *testing.T) {
|
|
skipIfNoDolt(t)
|
|
|
|
ctx := context.Background()
|
|
tmpDir, err := os.MkdirTemp("", "dolt-test-*")
|
|
if err != nil {
|
|
t.Fatalf("failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
cfg := &Config{
|
|
Path: tmpDir,
|
|
CommitterName: "test",
|
|
CommitterEmail: "test@example.com",
|
|
Database: "testdb",
|
|
}
|
|
|
|
store, err := New(ctx, cfg)
|
|
if err != nil {
|
|
t.Fatalf("failed to create Dolt store: %v", err)
|
|
}
|
|
defer store.Close()
|
|
|
|
// Verify store path
|
|
if store.Path() != tmpDir {
|
|
t.Errorf("expected path %s, got %s", tmpDir, store.Path())
|
|
}
|
|
|
|
// Verify not closed
|
|
if store.IsClosed() {
|
|
t.Error("store should not be closed")
|
|
}
|
|
}
|
|
|
|
func TestDoltStoreConfig(t *testing.T) {
|
|
store, cleanup := setupTestStore(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Test SetConfig
|
|
if err := store.SetConfig(ctx, "test_key", "test_value"); err != nil {
|
|
t.Fatalf("failed to set config: %v", err)
|
|
}
|
|
|
|
// Test GetConfig
|
|
value, err := store.GetConfig(ctx, "test_key")
|
|
if err != nil {
|
|
t.Fatalf("failed to get config: %v", err)
|
|
}
|
|
if value != "test_value" {
|
|
t.Errorf("expected 'test_value', got %q", value)
|
|
}
|
|
|
|
// Test GetAllConfig
|
|
allConfig, err := store.GetAllConfig(ctx)
|
|
if err != nil {
|
|
t.Fatalf("failed to get all config: %v", err)
|
|
}
|
|
if allConfig["test_key"] != "test_value" {
|
|
t.Errorf("expected test_key in all config")
|
|
}
|
|
|
|
// Test DeleteConfig
|
|
if err := store.DeleteConfig(ctx, "test_key"); err != nil {
|
|
t.Fatalf("failed to delete config: %v", err)
|
|
}
|
|
value, err = store.GetConfig(ctx, "test_key")
|
|
if err != nil {
|
|
t.Fatalf("failed to get deleted config: %v", err)
|
|
}
|
|
if value != "" {
|
|
t.Errorf("expected empty value after delete, got %q", value)
|
|
}
|
|
}
|
|
|
|
func TestDoltStoreIssue(t *testing.T) {
|
|
store, cleanup := setupTestStore(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Create an issue
|
|
issue := &types.Issue{
|
|
Title: "Test Issue",
|
|
Description: "Test description",
|
|
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)
|
|
}
|
|
|
|
// Verify ID was generated
|
|
if issue.ID == "" {
|
|
t.Error("expected issue ID to be generated")
|
|
}
|
|
|
|
// Get the issue back
|
|
retrieved, err := store.GetIssue(ctx, issue.ID)
|
|
if err != nil {
|
|
t.Fatalf("failed to get issue: %v", err)
|
|
}
|
|
if retrieved == nil {
|
|
t.Fatal("expected to retrieve issue")
|
|
}
|
|
if retrieved.Title != issue.Title {
|
|
t.Errorf("expected title %q, got %q", issue.Title, retrieved.Title)
|
|
}
|
|
}
|
|
|
|
func TestDoltStoreIssueUpdate(t *testing.T) {
|
|
store, cleanup := setupTestStore(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Create an issue
|
|
issue := &types.Issue{
|
|
Title: "Original Title",
|
|
Description: "Original description",
|
|
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)
|
|
}
|
|
|
|
// Update the issue
|
|
updates := map[string]interface{}{
|
|
"title": "Updated Title",
|
|
"priority": 1,
|
|
"status": string(types.StatusInProgress),
|
|
}
|
|
|
|
if err := store.UpdateIssue(ctx, issue.ID, updates, "tester"); err != nil {
|
|
t.Fatalf("failed to update issue: %v", err)
|
|
}
|
|
|
|
// Get the updated issue
|
|
retrieved, err := store.GetIssue(ctx, issue.ID)
|
|
if err != nil {
|
|
t.Fatalf("failed to get issue: %v", err)
|
|
}
|
|
if retrieved.Title != "Updated Title" {
|
|
t.Errorf("expected title 'Updated Title', got %q", retrieved.Title)
|
|
}
|
|
if retrieved.Priority != 1 {
|
|
t.Errorf("expected priority 1, got %d", retrieved.Priority)
|
|
}
|
|
if retrieved.Status != types.StatusInProgress {
|
|
t.Errorf("expected status in_progress, got %s", retrieved.Status)
|
|
}
|
|
}
|
|
|
|
func TestDoltStoreIssueClose(t *testing.T) {
|
|
store, cleanup := setupTestStore(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Create an issue
|
|
issue := &types.Issue{
|
|
Title: "Issue to Close",
|
|
Description: "Will be closed",
|
|
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)
|
|
}
|
|
|
|
// Close the issue
|
|
if err := store.CloseIssue(ctx, issue.ID, "completed", "tester", "session123"); err != nil {
|
|
t.Fatalf("failed to close issue: %v", err)
|
|
}
|
|
|
|
// Get the closed issue
|
|
retrieved, err := store.GetIssue(ctx, issue.ID)
|
|
if err != nil {
|
|
t.Fatalf("failed to get issue: %v", err)
|
|
}
|
|
if retrieved.Status != types.StatusClosed {
|
|
t.Errorf("expected status closed, got %s", retrieved.Status)
|
|
}
|
|
if retrieved.ClosedAt == nil {
|
|
t.Error("expected closed_at to be set")
|
|
}
|
|
}
|
|
|
|
func TestDoltStoreLabels(t *testing.T) {
|
|
store, cleanup := setupTestStore(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Create an issue
|
|
issue := &types.Issue{
|
|
Title: "Issue with Labels",
|
|
Description: "Test labels",
|
|
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)
|
|
}
|
|
|
|
// Add labels
|
|
if err := store.AddLabel(ctx, issue.ID, "bug", "tester"); err != nil {
|
|
t.Fatalf("failed to add label: %v", err)
|
|
}
|
|
if err := store.AddLabel(ctx, issue.ID, "priority", "tester"); err != nil {
|
|
t.Fatalf("failed to add second label: %v", err)
|
|
}
|
|
|
|
// Get labels
|
|
labels, err := store.GetLabels(ctx, issue.ID)
|
|
if err != nil {
|
|
t.Fatalf("failed to get labels: %v", err)
|
|
}
|
|
if len(labels) != 2 {
|
|
t.Errorf("expected 2 labels, got %d", len(labels))
|
|
}
|
|
|
|
// Remove label
|
|
if err := store.RemoveLabel(ctx, issue.ID, "bug", "tester"); err != nil {
|
|
t.Fatalf("failed to remove label: %v", err)
|
|
}
|
|
|
|
// Verify removal
|
|
labels, err = store.GetLabels(ctx, issue.ID)
|
|
if err != nil {
|
|
t.Fatalf("failed to get labels after removal: %v", err)
|
|
}
|
|
if len(labels) != 1 {
|
|
t.Errorf("expected 1 label after removal, got %d", len(labels))
|
|
}
|
|
}
|
|
|
|
func TestDoltStoreDependencies(t *testing.T) {
|
|
store, cleanup := setupTestStore(t)
|
|
defer cleanup()
|
|
|
|
ctx, cancel := testContext(t)
|
|
defer cancel()
|
|
|
|
// Create parent and child issues
|
|
parent := &types.Issue{
|
|
ID: "test-parent",
|
|
Title: "Parent Issue",
|
|
Description: "Parent description",
|
|
Status: types.StatusOpen,
|
|
Priority: 1,
|
|
IssueType: types.TypeEpic,
|
|
}
|
|
child := &types.Issue{
|
|
ID: "test-child",
|
|
Title: "Child Issue",
|
|
Description: "Child description",
|
|
Status: types.StatusOpen,
|
|
Priority: 2,
|
|
IssueType: types.TypeTask,
|
|
}
|
|
|
|
if err := store.CreateIssue(ctx, parent, "tester"); err != nil {
|
|
t.Fatalf("failed to create parent issue: %v", err)
|
|
}
|
|
if err := store.CreateIssue(ctx, child, "tester"); err != nil {
|
|
t.Fatalf("failed to create child issue: %v", err)
|
|
}
|
|
|
|
// Add dependency (child depends on parent)
|
|
dep := &types.Dependency{
|
|
IssueID: child.ID,
|
|
DependsOnID: parent.ID,
|
|
Type: types.DepBlocks,
|
|
}
|
|
if err := store.AddDependency(ctx, dep, "tester"); err != nil {
|
|
t.Fatalf("failed to add dependency: %v", err)
|
|
}
|
|
|
|
// Get dependencies
|
|
deps, err := store.GetDependencies(ctx, child.ID)
|
|
if err != nil {
|
|
t.Fatalf("failed to get dependencies: %v", err)
|
|
}
|
|
if len(deps) != 1 {
|
|
t.Errorf("expected 1 dependency, got %d", len(deps))
|
|
}
|
|
if deps[0].ID != parent.ID {
|
|
t.Errorf("expected dependency on %s, got %s", parent.ID, deps[0].ID)
|
|
}
|
|
|
|
// Get dependents
|
|
dependents, err := store.GetDependents(ctx, parent.ID)
|
|
if err != nil {
|
|
t.Fatalf("failed to get dependents: %v", err)
|
|
}
|
|
if len(dependents) != 1 {
|
|
t.Errorf("expected 1 dependent, got %d", len(dependents))
|
|
}
|
|
|
|
// Check if blocked
|
|
blocked, blockers, err := store.IsBlocked(ctx, child.ID)
|
|
if err != nil {
|
|
t.Fatalf("failed to check if blocked: %v", err)
|
|
}
|
|
if !blocked {
|
|
t.Error("expected child to be blocked")
|
|
}
|
|
if len(blockers) != 1 || blockers[0] != parent.ID {
|
|
t.Errorf("expected blocker %s, got %v", parent.ID, blockers)
|
|
}
|
|
|
|
// Remove dependency
|
|
if err := store.RemoveDependency(ctx, child.ID, parent.ID, "tester"); err != nil {
|
|
t.Fatalf("failed to remove dependency: %v", err)
|
|
}
|
|
|
|
// Verify removal
|
|
deps, err = store.GetDependencies(ctx, child.ID)
|
|
if err != nil {
|
|
t.Fatalf("failed to get dependencies after removal: %v", err)
|
|
}
|
|
if len(deps) != 0 {
|
|
t.Errorf("expected 0 dependencies after removal, got %d", len(deps))
|
|
}
|
|
}
|
|
|
|
func TestDoltStoreSearch(t *testing.T) {
|
|
store, cleanup := setupTestStore(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Create multiple issues
|
|
issues := []*types.Issue{
|
|
{
|
|
ID: "test-search-1",
|
|
Title: "First Issue",
|
|
Description: "Search test one",
|
|
Status: types.StatusOpen,
|
|
Priority: 1,
|
|
IssueType: types.TypeTask,
|
|
},
|
|
{
|
|
ID: "test-search-2",
|
|
Title: "Second Issue",
|
|
Description: "Search test two",
|
|
Status: types.StatusOpen,
|
|
Priority: 2,
|
|
IssueType: types.TypeBug,
|
|
},
|
|
{
|
|
ID: "test-search-3",
|
|
Title: "Third Issue",
|
|
Description: "Different content",
|
|
Status: types.StatusClosed,
|
|
Priority: 3,
|
|
IssueType: types.TypeTask,
|
|
},
|
|
}
|
|
|
|
for _, issue := range issues {
|
|
if err := store.CreateIssue(ctx, issue, "tester"); err != nil {
|
|
t.Fatalf("failed to create issue %s: %v", issue.ID, err)
|
|
}
|
|
}
|
|
|
|
// Search by query
|
|
results, err := store.SearchIssues(ctx, "Search test", types.IssueFilter{})
|
|
if err != nil {
|
|
t.Fatalf("failed to search issues: %v", err)
|
|
}
|
|
if len(results) != 2 {
|
|
t.Errorf("expected 2 results for 'Search test', got %d", len(results))
|
|
}
|
|
|
|
// Search with status filter
|
|
openStatus := types.StatusOpen
|
|
results, err = store.SearchIssues(ctx, "", types.IssueFilter{Status: &openStatus})
|
|
if err != nil {
|
|
t.Fatalf("failed to search with status filter: %v", err)
|
|
}
|
|
if len(results) != 2 {
|
|
t.Errorf("expected 2 open issues, got %d", len(results))
|
|
}
|
|
|
|
// Search by issue type
|
|
bugType := types.TypeBug
|
|
results, err = store.SearchIssues(ctx, "", types.IssueFilter{IssueType: &bugType})
|
|
if err != nil {
|
|
t.Fatalf("failed to search by type: %v", err)
|
|
}
|
|
if len(results) != 1 {
|
|
t.Errorf("expected 1 bug, got %d", len(results))
|
|
}
|
|
}
|
|
|
|
func TestDoltStoreCreateIssues(t *testing.T) {
|
|
store, cleanup := setupTestStore(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Create multiple issues in batch
|
|
issues := []*types.Issue{
|
|
{
|
|
ID: "test-batch-1",
|
|
Title: "Batch Issue 1",
|
|
Description: "First batch issue",
|
|
Status: types.StatusOpen,
|
|
Priority: 1,
|
|
IssueType: types.TypeTask,
|
|
},
|
|
{
|
|
ID: "test-batch-2",
|
|
Title: "Batch Issue 2",
|
|
Description: "Second batch issue",
|
|
Status: types.StatusOpen,
|
|
Priority: 2,
|
|
IssueType: types.TypeTask,
|
|
},
|
|
}
|
|
|
|
if err := store.CreateIssues(ctx, issues, "tester"); err != nil {
|
|
t.Fatalf("failed to create issues: %v", err)
|
|
}
|
|
|
|
// Verify all issues were created
|
|
for _, issue := range issues {
|
|
retrieved, err := store.GetIssue(ctx, issue.ID)
|
|
if err != nil {
|
|
t.Fatalf("failed to get issue %s: %v", issue.ID, err)
|
|
}
|
|
if retrieved == nil {
|
|
t.Errorf("expected to retrieve issue %s", issue.ID)
|
|
}
|
|
if retrieved.Title != issue.Title {
|
|
t.Errorf("expected title %q, got %q", issue.Title, retrieved.Title)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDoltStoreComments(t *testing.T) {
|
|
store, cleanup := setupTestStore(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Create an issue
|
|
issue := &types.Issue{
|
|
ID: "test-comment-issue",
|
|
Title: "Issue with Comments",
|
|
Description: "Test comments",
|
|
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)
|
|
}
|
|
|
|
// Add comments
|
|
comment1, err := store.AddIssueComment(ctx, issue.ID, "user1", "First comment")
|
|
if err != nil {
|
|
t.Fatalf("failed to add first comment: %v", err)
|
|
}
|
|
if comment1.ID == 0 {
|
|
t.Error("expected comment ID to be generated")
|
|
}
|
|
|
|
_, err = store.AddIssueComment(ctx, issue.ID, "user2", "Second comment")
|
|
if err != nil {
|
|
t.Fatalf("failed to add second comment: %v", err)
|
|
}
|
|
|
|
// Get comments
|
|
comments, err := store.GetIssueComments(ctx, issue.ID)
|
|
if err != nil {
|
|
t.Fatalf("failed to get comments: %v", err)
|
|
}
|
|
if len(comments) != 2 {
|
|
t.Errorf("expected 2 comments, got %d", len(comments))
|
|
}
|
|
if comments[0].Text != "First comment" {
|
|
t.Errorf("expected 'First comment', got %q", comments[0].Text)
|
|
}
|
|
}
|
|
|
|
func TestDoltStoreEvents(t *testing.T) {
|
|
store, cleanup := setupTestStore(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Create an issue (this creates a creation event)
|
|
issue := &types.Issue{
|
|
ID: "test-event-issue",
|
|
Title: "Issue with Events",
|
|
Description: "Test events",
|
|
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)
|
|
}
|
|
|
|
// Add a comment event
|
|
if err := store.AddComment(ctx, issue.ID, "user1", "A comment"); err != nil {
|
|
t.Fatalf("failed to add comment: %v", err)
|
|
}
|
|
|
|
// Get events
|
|
events, err := store.GetEvents(ctx, issue.ID, 10)
|
|
if err != nil {
|
|
t.Fatalf("failed to get events: %v", err)
|
|
}
|
|
if len(events) < 2 {
|
|
t.Errorf("expected at least 2 events, got %d", len(events))
|
|
}
|
|
}
|
|
|
|
func TestDoltStoreDeleteIssue(t *testing.T) {
|
|
store, cleanup := setupTestStore(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Create an issue
|
|
issue := &types.Issue{
|
|
ID: "test-delete-issue",
|
|
Title: "Issue to Delete",
|
|
Description: "Will be deleted",
|
|
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)
|
|
}
|
|
|
|
// Verify it exists
|
|
retrieved, err := store.GetIssue(ctx, issue.ID)
|
|
if err != nil || retrieved == nil {
|
|
t.Fatalf("issue should exist before delete")
|
|
}
|
|
|
|
// Delete the issue
|
|
if err := store.DeleteIssue(ctx, issue.ID); err != nil {
|
|
t.Fatalf("failed to delete issue: %v", err)
|
|
}
|
|
|
|
// Verify it's gone
|
|
retrieved, err = store.GetIssue(ctx, issue.ID)
|
|
if err != nil {
|
|
t.Fatalf("failed to get issue after delete: %v", err)
|
|
}
|
|
if retrieved != nil {
|
|
t.Error("expected issue to be deleted")
|
|
}
|
|
}
|
|
|
|
func TestDoltStoreDirtyTracking(t *testing.T) {
|
|
store, cleanup := setupTestStore(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Create an issue (marks it dirty)
|
|
issue := &types.Issue{
|
|
ID: "test-dirty-issue",
|
|
Title: "Dirty Issue",
|
|
Description: "Will be dirty",
|
|
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)
|
|
}
|
|
|
|
// Get dirty issues
|
|
dirtyIDs, err := store.GetDirtyIssues(ctx)
|
|
if err != nil {
|
|
t.Fatalf("failed to get dirty issues: %v", err)
|
|
}
|
|
found := false
|
|
for _, id := range dirtyIDs {
|
|
if id == issue.ID {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("expected issue to be in dirty list")
|
|
}
|
|
|
|
// Clear dirty issues
|
|
if err := store.ClearDirtyIssuesByID(ctx, []string{issue.ID}); err != nil {
|
|
t.Fatalf("failed to clear dirty issues: %v", err)
|
|
}
|
|
|
|
// Verify it's cleared
|
|
dirtyIDs, err = store.GetDirtyIssues(ctx)
|
|
if err != nil {
|
|
t.Fatalf("failed to get dirty issues after clear: %v", err)
|
|
}
|
|
for _, id := range dirtyIDs {
|
|
if id == issue.ID {
|
|
t.Error("expected issue to be cleared from dirty list")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDoltStoreStatistics(t *testing.T) {
|
|
store, cleanup := setupTestStore(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Create some issues
|
|
issues := []*types.Issue{
|
|
{ID: "test-stat-1", Title: "Open 1", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask},
|
|
{ID: "test-stat-2", Title: "Open 2", Status: types.StatusOpen, Priority: 2, IssueType: types.TypeTask},
|
|
{ID: "test-stat-3", Title: "Closed", Status: types.StatusClosed, Priority: 1, IssueType: types.TypeTask},
|
|
}
|
|
|
|
for _, issue := range issues {
|
|
if err := store.CreateIssue(ctx, issue, "tester"); err != nil {
|
|
t.Fatalf("failed to create issue: %v", err)
|
|
}
|
|
}
|
|
|
|
// Get statistics
|
|
stats, err := store.GetStatistics(ctx)
|
|
if err != nil {
|
|
t.Fatalf("failed to get statistics: %v", err)
|
|
}
|
|
|
|
if stats.OpenIssues < 2 {
|
|
t.Errorf("expected at least 2 open issues, got %d", stats.OpenIssues)
|
|
}
|
|
if stats.ClosedIssues < 1 {
|
|
t.Errorf("expected at least 1 closed issue, got %d", stats.ClosedIssues)
|
|
}
|
|
}
|
|
|
|
// Test SQL injection protection
|
|
|
|
func TestValidateRef(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
ref string
|
|
wantErr bool
|
|
}{
|
|
{"valid hash", "abc123def456", false},
|
|
{"valid branch", "main", false},
|
|
{"valid with underscore", "feature_branch", false},
|
|
{"valid with dash", "feature-branch", false},
|
|
{"empty", "", true},
|
|
{"too long", string(make([]byte, 200)), true},
|
|
{"with SQL injection", "main'; DROP TABLE issues; --", true},
|
|
{"with quotes", "main'test", true},
|
|
{"with semicolon", "main;test", true},
|
|
{"with space", "main test", true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := validateRef(tt.ref)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("validateRef(%q) error = %v, wantErr %v", tt.ref, err, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateTableName(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
tableName string
|
|
wantErr bool
|
|
}{
|
|
{"valid table", "issues", false},
|
|
{"valid with underscore", "dirty_issues", false},
|
|
{"valid with numbers", "table123", false},
|
|
{"empty", "", true},
|
|
{"too long", string(make([]byte, 100)), true},
|
|
{"starts with number", "123table", true},
|
|
{"with SQL injection", "issues'; DROP TABLE issues; --", true},
|
|
{"with space", "my table", true},
|
|
{"with dash", "my-table", true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := validateTableName(tt.tableName)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("validateTableName(%q) error = %v, wantErr %v", tt.tableName, err, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDoltStoreGetReadyWork(t *testing.T) {
|
|
store, cleanup := setupTestStore(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Create issues: one blocked, one ready
|
|
blocker := &types.Issue{
|
|
ID: "test-blocker",
|
|
Title: "Blocker",
|
|
Description: "Blocks another issue",
|
|
Status: types.StatusOpen,
|
|
Priority: 1,
|
|
IssueType: types.TypeTask,
|
|
}
|
|
blocked := &types.Issue{
|
|
ID: "test-blocked",
|
|
Title: "Blocked",
|
|
Description: "Is blocked",
|
|
Status: types.StatusOpen,
|
|
Priority: 2,
|
|
IssueType: types.TypeTask,
|
|
}
|
|
ready := &types.Issue{
|
|
ID: "test-ready",
|
|
Title: "Ready",
|
|
Description: "Is ready",
|
|
Status: types.StatusOpen,
|
|
Priority: 3,
|
|
IssueType: types.TypeTask,
|
|
}
|
|
|
|
for _, issue := range []*types.Issue{blocker, blocked, ready} {
|
|
if err := store.CreateIssue(ctx, issue, "tester"); err != nil {
|
|
t.Fatalf("failed to create issue %s: %v", issue.ID, err)
|
|
}
|
|
}
|
|
|
|
// Add blocking dependency
|
|
dep := &types.Dependency{
|
|
IssueID: blocked.ID,
|
|
DependsOnID: blocker.ID,
|
|
Type: types.DepBlocks,
|
|
}
|
|
if err := store.AddDependency(ctx, dep, "tester"); err != nil {
|
|
t.Fatalf("failed to add dependency: %v", err)
|
|
}
|
|
|
|
// Get ready work
|
|
readyWork, err := store.GetReadyWork(ctx, types.WorkFilter{})
|
|
if err != nil {
|
|
t.Fatalf("failed to get ready work: %v", err)
|
|
}
|
|
|
|
// Should include blocker and ready, but not blocked
|
|
foundBlocker := false
|
|
foundBlocked := false
|
|
foundReady := false
|
|
for _, issue := range readyWork {
|
|
switch issue.ID {
|
|
case blocker.ID:
|
|
foundBlocker = true
|
|
case blocked.ID:
|
|
foundBlocked = true
|
|
case ready.ID:
|
|
foundReady = true
|
|
}
|
|
}
|
|
|
|
if !foundBlocker {
|
|
t.Error("expected blocker to be in ready work")
|
|
}
|
|
if foundBlocked {
|
|
t.Error("expected blocked issue to NOT be in ready work")
|
|
}
|
|
if !foundReady {
|
|
t.Error("expected ready issue to be in ready work")
|
|
}
|
|
}
|