Files
beads/internal/storage/sqlite/transaction_test.go
collins 7cf67153de refactor(types): remove Gas Town type constants from beads core (bd-w2zz4)
Remove Gas Town-specific type constants (TypeMolecule, TypeGate, TypeConvoy,
TypeMergeRequest, TypeSlot, TypeAgent, TypeRole, TypeRig, TypeEvent, TypeMessage)
from internal/types/types.go.

Beads now only has core work types built-in:
- bug, feature, task, epic, chore

All Gas Town types are now purely custom types with no special handling in beads.
Use string literals like "gate" or "molecule" when needed, and configure
types.custom in config.yaml for validation.

Changes:
- Remove Gas Town type constants from types.go
- Remove mr/mol aliases from Normalize()
- Update bd types command to only show core types
- Replace all constant usages with string literals throughout codebase
- Update tests to use string literals

This decouples beads from Gas Town, making it a generic issue tracker.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 10:36:59 -08:00

2010 lines
54 KiB
Go

package sqlite
import (
"context"
"fmt"
"testing"
"time"
"github.com/steveyegge/beads/internal/storage"
"github.com/steveyegge/beads/internal/types"
)
// TestRunInTransactionBasic verifies the RunInTransaction method exists and
// can be called.
func TestRunInTransactionBasic(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
// Test that we can call RunInTransaction
callCount := 0
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
callCount++
return nil
})
if err != nil {
t.Errorf("RunInTransaction returned error: %v", err)
}
if callCount != 1 {
t.Errorf("expected callback to be called once, got %d", callCount)
}
}
// TestRunInTransactionRollbackOnError verifies that returning an error
// from the callback does not cause a panic and the error is propagated.
func TestRunInTransactionRollbackOnError(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
expectedErr := "intentional test error"
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
return &testError{msg: expectedErr}
})
if err == nil {
t.Error("expected error to be returned, got nil")
}
if err.Error() != expectedErr {
t.Errorf("expected error %q, got %q", expectedErr, err.Error())
}
}
// TestRunInTransactionPanicRecovery verifies that panics in the callback
// are recovered and re-raised after rollback.
func TestRunInTransactionPanicRecovery(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
defer func() {
if r := recover(); r == nil {
t.Error("expected panic to be re-raised, but no panic occurred")
} else if r != "test panic" {
t.Errorf("unexpected panic value: %v", r)
}
}()
_ = store.RunInTransaction(ctx, func(tx storage.Transaction) error {
panic("test panic")
})
t.Error("should not reach here - panic should have been re-raised")
}
// TestTransactionCreateIssue tests creating an issue within a transaction.
func TestTransactionCreateIssue(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
var createdID string
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
issue := &types.Issue{
Title: "Test Issue",
Status: types.StatusOpen,
Priority: 2,
IssueType: types.TypeTask,
}
if err := tx.CreateIssue(ctx, issue, "test-actor"); err != nil {
return err
}
createdID = issue.ID
return nil
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
if createdID == "" {
t.Error("expected issue ID to be set after creation")
}
// Verify issue exists after commit
issue, err := store.GetIssue(ctx, createdID)
if err != nil {
t.Fatalf("GetIssue failed: %v", err)
}
if issue == nil {
t.Error("expected issue to exist after transaction commit")
}
if issue.Title != "Test Issue" {
t.Errorf("expected title 'Test Issue', got %q", issue.Title)
}
}
// TestTransactionRollbackOnCreateError tests that issues are not created
// when transaction rolls back due to error.
func TestTransactionRollbackOnCreateError(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
var createdID string
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
issue := &types.Issue{
Title: "Test Issue",
Status: types.StatusOpen,
Priority: 2,
IssueType: types.TypeTask,
}
if err := tx.CreateIssue(ctx, issue, "test-actor"); err != nil {
return err
}
createdID = issue.ID
// Return error to trigger rollback
return &testError{msg: "intentional rollback"}
})
if err == nil {
t.Error("expected error from transaction")
}
// Verify issue does NOT exist after rollback
if createdID != "" {
issue, err := store.GetIssue(ctx, createdID)
if err != nil {
t.Fatalf("GetIssue failed: %v", err)
}
if issue != nil {
t.Error("expected issue to NOT exist after transaction rollback")
}
}
}
// TestTransactionMultipleIssues tests creating multiple issues atomically.
func TestTransactionMultipleIssues(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
var ids []string
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
for i := 0; i < 3; i++ {
issue := &types.Issue{
Title: "Test Issue",
Status: types.StatusOpen,
Priority: 2,
IssueType: types.TypeTask,
}
if err := tx.CreateIssue(ctx, issue, "test-actor"); err != nil {
return err
}
ids = append(ids, issue.ID)
}
return nil
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
// Verify all issues exist
for _, id := range ids {
issue, err := store.GetIssue(ctx, id)
if err != nil {
t.Fatalf("GetIssue failed for %s: %v", id, err)
}
if issue == nil {
t.Errorf("expected issue %s to exist", id)
}
}
}
// TestTransactionUpdateIssue tests updating an issue within a transaction.
func TestTransactionUpdateIssue(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
// Create issue first
issue := &types.Issue{
Title: "Original Title",
Status: types.StatusOpen,
Priority: 2,
IssueType: types.TypeTask,
}
if err := store.CreateIssue(ctx, issue, "test-actor"); err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
// Update in transaction
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
return tx.UpdateIssue(ctx, issue.ID, map[string]interface{}{
"title": "Updated Title",
}, "test-actor")
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
// Verify update
updated, err := store.GetIssue(ctx, issue.ID)
if err != nil {
t.Fatalf("GetIssue failed: %v", err)
}
if updated.Title != "Updated Title" {
t.Errorf("expected title 'Updated Title', got %q", updated.Title)
}
}
// TestTransactionCloseIssue tests closing an issue within a transaction.
func TestTransactionCloseIssue(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
// Create issue first
issue := &types.Issue{
Title: "Test Issue",
Status: types.StatusOpen,
Priority: 2,
IssueType: types.TypeTask,
}
if err := store.CreateIssue(ctx, issue, "test-actor"); err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
// Close in transaction
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
return tx.CloseIssue(ctx, issue.ID, "Done", "test-actor", "")
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
// Verify closed
closed, err := store.GetIssue(ctx, issue.ID)
if err != nil {
t.Fatalf("GetIssue failed: %v", err)
}
if closed.Status != types.StatusClosed {
t.Errorf("expected status 'closed', got %q", closed.Status)
}
if closed.CloseReason != "Done" {
t.Errorf("expected close_reason 'Done', got %q", closed.CloseReason)
}
}
// TestTransactionDeleteIssue tests deleting an issue within a transaction.
func TestTransactionDeleteIssue(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
// Create issue first
issue := &types.Issue{
Title: "Test Issue",
Status: types.StatusOpen,
Priority: 2,
IssueType: types.TypeTask,
}
if err := store.CreateIssue(ctx, issue, "test-actor"); err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
// Delete in transaction
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
return tx.DeleteIssue(ctx, issue.ID)
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
// Verify deleted
deleted, err := store.GetIssue(ctx, issue.ID)
if err != nil {
t.Fatalf("GetIssue failed: %v", err)
}
if deleted != nil {
t.Error("expected issue to be deleted")
}
}
// TestTransactionGetIssue tests read-your-writes within a transaction.
func TestTransactionGetIssue(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
// Create issue
issue := &types.Issue{
Title: "Test Issue",
Status: types.StatusOpen,
Priority: 2,
IssueType: types.TypeTask,
}
if err := tx.CreateIssue(ctx, issue, "test-actor"); err != nil {
return err
}
// Read it back within same transaction (read-your-writes)
retrieved, err := tx.GetIssue(ctx, issue.ID)
if err != nil {
return err
}
if retrieved == nil {
t.Error("expected to read issue within transaction")
}
if retrieved.Title != "Test Issue" {
t.Errorf("expected title 'Test Issue', got %q", retrieved.Title)
}
return nil
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
}
// TestTransactionCreateIssues tests batch issue creation within a transaction.
func TestTransactionCreateIssues(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
var ids []string
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
issues := []*types.Issue{
{Title: "Issue 1", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask},
{Title: "Issue 2", Status: types.StatusOpen, Priority: 2, IssueType: types.TypeTask},
{Title: "Issue 3", Status: types.StatusOpen, Priority: 3, IssueType: types.TypeTask},
}
if err := tx.CreateIssues(ctx, issues, "test-actor"); err != nil {
return err
}
for _, issue := range issues {
ids = append(ids, issue.ID)
}
return nil
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
// Verify all issues exist
for i, id := range ids {
issue, err := store.GetIssue(ctx, id)
if err != nil {
t.Fatalf("GetIssue failed for %s: %v", id, err)
}
if issue == nil {
t.Errorf("expected issue %s to exist", id)
}
expectedTitle := "Issue " + string(rune('1'+i))
if issue.Title != expectedTitle {
t.Errorf("expected title %q, got %q", expectedTitle, issue.Title)
}
}
}
// TestTransactionAddDependency tests adding a dependency within a transaction.
func TestTransactionAddDependency(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
// Create two issues first
issue1 := &types.Issue{Title: "Issue 1", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
issue2 := &types.Issue{Title: "Issue 2", Status: types.StatusOpen, Priority: 2, IssueType: types.TypeTask}
if err := store.CreateIssue(ctx, issue1, "test-actor"); err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
if err := store.CreateIssue(ctx, issue2, "test-actor"); err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
// Add dependency in transaction
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
dep := &types.Dependency{
IssueID: issue1.ID,
DependsOnID: issue2.ID,
Type: types.DepBlocks,
}
return tx.AddDependency(ctx, dep, "test-actor")
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
// Verify dependency exists
deps, err := store.GetDependencies(ctx, issue1.ID)
if err != nil {
t.Fatalf("GetDependencies failed: %v", err)
}
if len(deps) != 1 {
t.Errorf("expected 1 dependency, got %d", len(deps))
}
if deps[0].ID != issue2.ID {
t.Errorf("expected dependency on %s, got %s", issue2.ID, deps[0].ID)
}
}
// TestTransactionAddDependency_RelatesTo tests that bidirectional relates-to
// dependencies work in transaction context. This is a regression test for
// Decision 004 Phase 4 - the cycle detection must exempt relates-to type
// since bidirectional relationships are semantically valid.
func TestTransactionAddDependency_RelatesTo(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
// Create two issues
issue1 := &types.Issue{Title: "Issue 1", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
issue2 := &types.Issue{Title: "Issue 2", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
if err := store.CreateIssue(ctx, issue1, "test-actor"); err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
if err := store.CreateIssue(ctx, issue2, "test-actor"); err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
// Add bidirectional relates-to in a single transaction
// This should NOT fail cycle detection since relates-to is exempt
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
// First direction: issue1 relates-to issue2
dep1 := &types.Dependency{
IssueID: issue1.ID,
DependsOnID: issue2.ID,
Type: types.DepRelatesTo,
}
if err := tx.AddDependency(ctx, dep1, "test-actor"); err != nil {
return fmt.Errorf("first relates-to failed: %w", err)
}
// Second direction: issue2 relates-to issue1 (would be a cycle for other types)
dep2 := &types.Dependency{
IssueID: issue2.ID,
DependsOnID: issue1.ID,
Type: types.DepRelatesTo,
}
if err := tx.AddDependency(ctx, dep2, "test-actor"); err != nil {
return fmt.Errorf("second relates-to failed: %w", err)
}
return nil
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
// Verify both directions exist
deps1, err := store.GetDependenciesWithMetadata(ctx, issue1.ID)
if err != nil {
t.Fatalf("GetDependenciesWithMetadata failed: %v", err)
}
found1 := false
for _, d := range deps1 {
if d.ID == issue2.ID && d.DependencyType == types.DepRelatesTo {
found1 = true
}
}
if !found1 {
t.Errorf("issue1 should have relates-to link to issue2")
}
deps2, err := store.GetDependenciesWithMetadata(ctx, issue2.ID)
if err != nil {
t.Fatalf("GetDependenciesWithMetadata failed: %v", err)
}
found2 := false
for _, d := range deps2 {
if d.ID == issue1.ID && d.DependencyType == types.DepRelatesTo {
found2 = true
}
}
if !found2 {
t.Errorf("issue2 should have relates-to link to issue1")
}
}
// TestTransactionAddDependency_RepliesTo tests that replies-to dependencies
// preserve thread_id in transaction context (Decision 004 Phase 4).
func TestTransactionAddDependency_RepliesTo(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
// Create original message and reply
original := &types.Issue{
Title: "Original Message",
Status: types.StatusOpen,
Priority: 2,
IssueType: "message",
Sender: "alice",
}
reply := &types.Issue{
Title: "Re: Original Message",
Status: types.StatusOpen,
Priority: 2,
IssueType: "message",
Sender: "bob",
}
if err := store.CreateIssue(ctx, original, "test-actor"); err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
if err := store.CreateIssue(ctx, reply, "test-actor"); err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
// Add replies-to with thread_id in transaction
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
dep := &types.Dependency{
IssueID: reply.ID,
DependsOnID: original.ID,
Type: types.DepRepliesTo,
ThreadID: original.ID, // Thread root
}
return tx.AddDependency(ctx, dep, "test-actor")
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
// Verify the dependency and thread_id were preserved
deps, err := store.GetDependenciesWithMetadata(ctx, reply.ID)
if err != nil {
t.Fatalf("GetDependenciesWithMetadata failed: %v", err)
}
found := false
for _, d := range deps {
if d.ID == original.ID && d.DependencyType == types.DepRepliesTo {
found = true
}
}
if !found {
t.Errorf("reply should have replies-to link to original")
}
// Verify thread_id by querying dependencies table directly
var threadID string
err = store.UnderlyingDB().QueryRowContext(ctx,
`SELECT thread_id FROM dependencies WHERE issue_id = ? AND depends_on_id = ?`,
reply.ID, original.ID).Scan(&threadID)
if err != nil {
t.Fatalf("Failed to query thread_id: %v", err)
}
if threadID != original.ID {
t.Errorf("thread_id = %q, want %q", threadID, original.ID)
}
}
// TestTransactionRemoveDependency tests removing a dependency within a transaction.
func TestTransactionRemoveDependency(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
// Create two issues and add dependency
issue1 := &types.Issue{Title: "Issue 1", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
issue2 := &types.Issue{Title: "Issue 2", Status: types.StatusOpen, Priority: 2, IssueType: types.TypeTask}
if err := store.CreateIssue(ctx, issue1, "test-actor"); err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
if err := store.CreateIssue(ctx, issue2, "test-actor"); err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
dep := &types.Dependency{IssueID: issue1.ID, DependsOnID: issue2.ID, Type: types.DepBlocks}
if err := store.AddDependency(ctx, dep, "test-actor"); err != nil {
t.Fatalf("AddDependency failed: %v", err)
}
// Remove dependency in transaction
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
return tx.RemoveDependency(ctx, issue1.ID, issue2.ID, "test-actor")
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
// Verify dependency is gone
deps, err := store.GetDependencies(ctx, issue1.ID)
if err != nil {
t.Fatalf("GetDependencies failed: %v", err)
}
if len(deps) != 0 {
t.Errorf("expected 0 dependencies, got %d", len(deps))
}
}
// TestTransactionAddLabel tests adding a label within a transaction.
func TestTransactionAddLabel(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
// Create issue first
issue := &types.Issue{Title: "Test Issue", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
if err := store.CreateIssue(ctx, issue, "test-actor"); err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
// Add label in transaction
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
return tx.AddLabel(ctx, issue.ID, "test-label", "test-actor")
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
// Verify label exists
labels, err := store.GetLabels(ctx, issue.ID)
if err != nil {
t.Fatalf("GetLabels failed: %v", err)
}
if len(labels) != 1 {
t.Errorf("expected 1 label, got %d", len(labels))
}
if labels[0] != "test-label" {
t.Errorf("expected label 'test-label', got %s", labels[0])
}
}
// TestTransactionRemoveLabel tests removing a label within a transaction.
func TestTransactionRemoveLabel(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
// Create issue and add label
issue := &types.Issue{Title: "Test Issue", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
if err := store.CreateIssue(ctx, issue, "test-actor"); err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
if err := store.AddLabel(ctx, issue.ID, "test-label", "test-actor"); err != nil {
t.Fatalf("AddLabel failed: %v", err)
}
// Remove label in transaction
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
return tx.RemoveLabel(ctx, issue.ID, "test-label", "test-actor")
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
// Verify label is gone
labels, err := store.GetLabels(ctx, issue.ID)
if err != nil {
t.Fatalf("GetLabels failed: %v", err)
}
if len(labels) != 0 {
t.Errorf("expected 0 labels, got %d", len(labels))
}
}
// TestTransactionAtomicIssueWithDependency tests creating issue + adding dependency atomically.
func TestTransactionAtomicIssueWithDependency(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
// Create parent issue first
parent := &types.Issue{Title: "Parent", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
if err := store.CreateIssue(ctx, parent, "test-actor"); err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
var childID string
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
// Create child issue
child := &types.Issue{Title: "Child", Status: types.StatusOpen, Priority: 2, IssueType: types.TypeTask}
if err := tx.CreateIssue(ctx, child, "test-actor"); err != nil {
return err
}
childID = child.ID
// Add dependency: child blocks parent (child must be done before parent)
dep := &types.Dependency{
IssueID: parent.ID,
DependsOnID: child.ID,
Type: types.DepBlocks,
}
return tx.AddDependency(ctx, dep, "test-actor")
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
// Verify both issue and dependency exist
child, err := store.GetIssue(ctx, childID)
if err != nil || child == nil {
t.Error("expected child issue to exist")
}
deps, err := store.GetDependencies(ctx, parent.ID)
if err != nil {
t.Fatalf("GetDependencies failed: %v", err)
}
if len(deps) != 1 || deps[0].ID != childID {
t.Error("expected dependency from parent to child")
}
}
// TestTransactionAtomicIssueWithLabels tests creating issue + adding labels atomically.
func TestTransactionAtomicIssueWithLabels(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
var issueID string
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
// Create issue
issue := &types.Issue{Title: "Test Issue", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
if err := tx.CreateIssue(ctx, issue, "test-actor"); err != nil {
return err
}
issueID = issue.ID
// Add multiple labels
for _, label := range []string{"label1", "label2", "label3"} {
if err := tx.AddLabel(ctx, issue.ID, label, "test-actor"); err != nil {
return err
}
}
return nil
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
// Verify issue and all labels exist
issue, err := store.GetIssue(ctx, issueID)
if err != nil || issue == nil {
t.Error("expected issue to exist")
}
labels, err := store.GetLabels(ctx, issueID)
if err != nil {
t.Fatalf("GetLabels failed: %v", err)
}
if len(labels) != 3 {
t.Errorf("expected 3 labels, got %d", len(labels))
}
}
// TestTransactionEmpty tests that an empty transaction commits successfully.
func TestTransactionEmpty(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
// Do nothing - empty transaction
return nil
})
if err != nil {
t.Errorf("empty transaction should succeed, got error: %v", err)
}
}
// TestTransactionConcurrent tests multiple concurrent transactions.
func TestTransactionConcurrent(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
const numGoroutines = 10
errors := make(chan error, numGoroutines)
ids := make(chan string, numGoroutines)
// Launch concurrent transactions
for i := 0; i < numGoroutines; i++ {
go func(index int) {
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
issue := &types.Issue{
Title: "Concurrent Issue",
Status: types.StatusOpen,
Priority: index % 4,
IssueType: types.TypeTask,
}
if err := tx.CreateIssue(ctx, issue, "test-actor"); err != nil {
return err
}
ids <- issue.ID
return nil
})
errors <- err
}(i)
}
// Collect results
var errs []error
var createdIDs []string
for i := 0; i < numGoroutines; i++ {
if err := <-errors; err != nil {
errs = append(errs, err)
}
}
close(ids)
for id := range ids {
createdIDs = append(createdIDs, id)
}
if len(errs) > 0 {
t.Errorf("some transactions failed: %v", errs)
}
if len(createdIDs) != numGoroutines {
t.Errorf("expected %d issues created, got %d", numGoroutines, len(createdIDs))
}
// Verify all issues exist
for _, id := range createdIDs {
issue, err := store.GetIssue(ctx, id)
if err != nil || issue == nil {
t.Errorf("expected issue %s to exist", id)
}
}
}
// TestTransactionNestedFailure tests that when first op succeeds but second fails,
// both are rolled back.
func TestTransactionNestedFailure(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
var firstIssueID string
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
// First operation succeeds
issue1 := &types.Issue{
Title: "First Issue",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
}
if err := tx.CreateIssue(ctx, issue1, "test-actor"); err != nil {
return err
}
firstIssueID = issue1.ID
// Second operation fails
issue2 := &types.Issue{
Title: "", // Invalid - missing title
Status: types.StatusOpen,
Priority: 2,
}
return tx.CreateIssue(ctx, issue2, "test-actor")
})
if err == nil {
t.Error("expected error from invalid second issue")
}
// Verify first issue was NOT created (rolled back)
if firstIssueID != "" {
issue, err := store.GetIssue(ctx, firstIssueID)
if err != nil {
t.Fatalf("GetIssue failed: %v", err)
}
if issue != nil {
t.Error("expected first issue to be rolled back, but it exists")
}
}
}
// TestTransactionAtomicPlanApproval simulates a VC plan approval workflow:
// creating multiple issues with dependencies and labels atomically.
func TestTransactionAtomicPlanApproval(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
var epicID, task1ID, task2ID string
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
// Create epic
epic := &types.Issue{
Title: "Epic: Feature Implementation",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeEpic,
}
if err := tx.CreateIssue(ctx, epic, "test-actor"); err != nil {
return err
}
epicID = epic.ID
// Create task 1
task1 := &types.Issue{
Title: "Task 1: Setup",
Status: types.StatusOpen,
Priority: 2,
IssueType: types.TypeTask,
}
if err := tx.CreateIssue(ctx, task1, "test-actor"); err != nil {
return err
}
task1ID = task1.ID
// Create task 2
task2 := &types.Issue{
Title: "Task 2: Implementation",
Status: types.StatusOpen,
Priority: 2,
IssueType: types.TypeTask,
}
if err := tx.CreateIssue(ctx, task2, "test-actor"); err != nil {
return err
}
task2ID = task2.ID
// Add dependencies: task2 depends on task1
dep := &types.Dependency{
IssueID: task2ID,
DependsOnID: task1ID,
Type: types.DepBlocks,
}
if err := tx.AddDependency(ctx, dep, "test-actor"); err != nil {
return err
}
// Add labels to all issues
for _, id := range []string{epicID, task1ID, task2ID} {
if err := tx.AddLabel(ctx, id, "feature-x", "test-actor"); err != nil {
return err
}
}
return nil
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
// Verify all issues exist
for _, id := range []string{epicID, task1ID, task2ID} {
issue, err := store.GetIssue(ctx, id)
if err != nil || issue == nil {
t.Errorf("expected issue %s to exist", id)
}
}
// Verify dependency
deps, err := store.GetDependencies(ctx, task2ID)
if err != nil {
t.Fatalf("GetDependencies failed: %v", err)
}
if len(deps) != 1 || deps[0].ID != task1ID {
t.Error("expected task2 to depend on task1")
}
// Verify labels
for _, id := range []string{epicID, task1ID, task2ID} {
labels, err := store.GetLabels(ctx, id)
if err != nil {
t.Fatalf("GetLabels failed: %v", err)
}
if len(labels) != 1 || labels[0] != "feature-x" {
t.Errorf("expected 'feature-x' label on %s", id)
}
}
}
// TestTransactionSetConfig tests setting a config value within a transaction.
func TestTransactionSetConfig(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
return tx.SetConfig(ctx, "test.key", "test-value")
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
// Verify config was set
value, err := store.GetConfig(ctx, "test.key")
if err != nil {
t.Fatalf("GetConfig failed: %v", err)
}
if value != "test-value" {
t.Errorf("expected 'test-value', got %q", value)
}
}
// TestTransactionGetConfig tests reading config within a transaction (read-your-writes).
func TestTransactionGetConfig(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
// Set config
if err := tx.SetConfig(ctx, "test.key", "test-value"); err != nil {
return err
}
// Read it back within same transaction
value, err := tx.GetConfig(ctx, "test.key")
if err != nil {
return err
}
if value != "test-value" {
t.Errorf("expected 'test-value' within transaction, got %q", value)
}
return nil
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
}
// TestTransactionConfigRollback tests that config changes are rolled back on error.
func TestTransactionConfigRollback(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
if err := tx.SetConfig(ctx, "test.key", "test-value"); err != nil {
return err
}
return &testError{msg: "intentional rollback"}
})
if err == nil {
t.Error("expected error from transaction")
}
// Verify config was NOT set (rolled back)
value, err := store.GetConfig(ctx, "test.key")
if err != nil {
t.Fatalf("GetConfig failed: %v", err)
}
if value != "" {
t.Errorf("expected empty value after rollback, got %q", value)
}
}
// TestTransactionSetMetadata tests setting a metadata value within a transaction.
func TestTransactionSetMetadata(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
return tx.SetMetadata(ctx, "test.metadata", "metadata-value")
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
// Verify metadata was set
value, err := store.GetMetadata(ctx, "test.metadata")
if err != nil {
t.Fatalf("GetMetadata failed: %v", err)
}
if value != "metadata-value" {
t.Errorf("expected 'metadata-value', got %q", value)
}
}
// TestTransactionGetMetadata tests reading metadata within a transaction (read-your-writes).
func TestTransactionGetMetadata(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
// Set metadata
if err := tx.SetMetadata(ctx, "test.metadata", "metadata-value"); err != nil {
return err
}
// Read it back within same transaction
value, err := tx.GetMetadata(ctx, "test.metadata")
if err != nil {
return err
}
if value != "metadata-value" {
t.Errorf("expected 'metadata-value' within transaction, got %q", value)
}
return nil
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
}
// TestTransactionMetadataRollback tests that metadata changes are rolled back on error.
func TestTransactionMetadataRollback(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
if err := tx.SetMetadata(ctx, "test.metadata", "metadata-value"); err != nil {
return err
}
return &testError{msg: "intentional rollback"}
})
if err == nil {
t.Error("expected error from transaction")
}
// Verify metadata was NOT set (rolled back)
value, err := store.GetMetadata(ctx, "test.metadata")
if err != nil {
t.Fatalf("GetMetadata failed: %v", err)
}
if value != "" {
t.Errorf("expected empty value after rollback, got %q", value)
}
}
// TestTransactionAddComment tests adding a comment within a transaction.
func TestTransactionAddComment(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
// Create issue first
issue := &types.Issue{
Title: "Test Issue",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
}
if err := store.CreateIssue(ctx, issue, "test-actor"); err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
// Add comment in transaction
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
return tx.AddComment(ctx, issue.ID, "commenter", "This is a test comment")
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
// Verify comment exists via events
events, err := store.GetEvents(ctx, issue.ID, 10)
if err != nil {
t.Fatalf("GetEvents failed: %v", err)
}
found := false
for _, e := range events {
if e.EventType == types.EventCommented && e.Comment != nil && *e.Comment == "This is a test comment" {
found = true
break
}
}
if !found {
t.Error("expected comment event to exist")
}
}
// TestTransactionAddCommentToCreatedIssue tests adding a comment to an issue created in the same transaction.
func TestTransactionAddCommentToCreatedIssue(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
var issueID string
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
// Create issue
issue := &types.Issue{
Title: "Test Issue",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
}
if err := tx.CreateIssue(ctx, issue, "test-actor"); err != nil {
return err
}
issueID = issue.ID
// Add comment to the issue we just created
return tx.AddComment(ctx, issue.ID, "commenter", "Comment on new issue")
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
// Verify both issue and comment exist
issue, err := store.GetIssue(ctx, issueID)
if err != nil || issue == nil {
t.Error("expected issue to exist")
}
events, err := store.GetEvents(ctx, issueID, 10)
if err != nil {
t.Fatalf("GetEvents failed: %v", err)
}
found := false
for _, e := range events {
if e.EventType == types.EventCommented && e.Comment != nil && *e.Comment == "Comment on new issue" {
found = true
break
}
}
if !found {
t.Error("expected comment event to exist")
}
}
// TestTransactionAddCommentNonexistentIssue tests that adding a comment to a nonexistent issue fails.
func TestTransactionAddCommentNonexistentIssue(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
return tx.AddComment(ctx, "nonexistent-id", "commenter", "This should fail")
})
if err == nil {
t.Error("expected error when commenting on nonexistent issue")
}
}
// TestTransactionCommentRollback tests that comments are rolled back on error.
func TestTransactionCommentRollback(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
// Create issue first
issue := &types.Issue{
Title: "Test Issue",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
}
if err := store.CreateIssue(ctx, issue, "test-actor"); err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
if err := tx.AddComment(ctx, issue.ID, "commenter", "This comment should be rolled back"); err != nil {
return err
}
return &testError{msg: "intentional rollback"}
})
if err == nil {
t.Error("expected error from transaction")
}
// Verify comment was NOT added (rolled back)
events, err := store.GetEvents(ctx, issue.ID, 10)
if err != nil {
t.Fatalf("GetEvents failed: %v", err)
}
for _, e := range events {
if e.EventType == types.EventCommented {
t.Error("expected no comment events after rollback")
}
}
}
// TestTransactionAtomicConfigWithIssue tests atomically creating an issue and setting config.
func TestTransactionAtomicConfigWithIssue(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
var issueID string
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
// Create issue
issue := &types.Issue{
Title: "Test Issue",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
}
if err := tx.CreateIssue(ctx, issue, "test-actor"); err != nil {
return err
}
issueID = issue.ID
// Set config referencing the issue
if err := tx.SetConfig(ctx, "last_created_issue", issue.ID); err != nil {
return err
}
// Set metadata
if err := tx.SetMetadata(ctx, "import_marker", "test-import-123"); err != nil {
return err
}
return nil
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
// Verify all three operations succeeded
issue, err := store.GetIssue(ctx, issueID)
if err != nil || issue == nil {
t.Error("expected issue to exist")
}
configValue, err := store.GetConfig(ctx, "last_created_issue")
if err != nil {
t.Fatalf("GetConfig failed: %v", err)
}
if configValue != issueID {
t.Errorf("expected config value %q, got %q", issueID, configValue)
}
metadataValue, err := store.GetMetadata(ctx, "import_marker")
if err != nil {
t.Fatalf("GetMetadata failed: %v", err)
}
if metadataValue != "test-import-123" {
t.Errorf("expected metadata value 'test-import-123', got %q", metadataValue)
}
}
// TestTransactionConfigOverwrite tests that SetConfig overwrites existing values.
func TestTransactionConfigOverwrite(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
// Set initial value
if err := store.SetConfig(ctx, "test.key", "initial"); err != nil {
t.Fatalf("SetConfig failed: %v", err)
}
// Overwrite in transaction
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
return tx.SetConfig(ctx, "test.key", "updated")
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
// Verify overwrite
value, err := store.GetConfig(ctx, "test.key")
if err != nil {
t.Fatalf("GetConfig failed: %v", err)
}
if value != "updated" {
t.Errorf("expected 'updated', got %q", value)
}
}
// TestTransactionGetConfigNonexistent tests getting a nonexistent config key returns empty string.
func TestTransactionGetConfigNonexistent(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
value, err := tx.GetConfig(ctx, "nonexistent.key")
if err != nil {
return err
}
if value != "" {
t.Errorf("expected empty string for nonexistent key, got %q", value)
}
return nil
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
}
// TestTransactionGetMetadataNonexistent tests getting a nonexistent metadata key returns empty string.
func TestTransactionGetMetadataNonexistent(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
value, err := tx.GetMetadata(ctx, "nonexistent.metadata")
if err != nil {
return err
}
if value != "" {
t.Errorf("expected empty string for nonexistent metadata, got %q", value)
}
return nil
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
}
// testError is a simple error type for testing
type testError struct {
msg string
}
func (e *testError) Error() string {
return e.msg
}
// TestTransactionSearchIssuesBasic tests basic search within a transaction.
func TestTransactionSearchIssuesBasic(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
// Create some issues first
closedAt := time.Now()
issues := []*types.Issue{
{Title: "Alpha task", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask},
{Title: "Beta task", Status: types.StatusOpen, Priority: 2, IssueType: types.TypeTask},
{Title: "Gamma feature", Status: types.StatusClosed, Priority: 3, IssueType: types.TypeFeature, ClosedAt: &closedAt},
}
for _, issue := range issues {
if err := store.CreateIssue(ctx, issue, "test-actor"); err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
}
// Search within transaction
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
// Search by query
results, err := tx.SearchIssues(ctx, "task", types.IssueFilter{})
if err != nil {
return err
}
if len(results) != 2 {
t.Errorf("expected 2 issues matching 'task', got %d", len(results))
}
// Search by status
closedStatus := types.StatusClosed
results, err = tx.SearchIssues(ctx, "", types.IssueFilter{Status: &closedStatus})
if err != nil {
return err
}
if len(results) != 1 {
t.Errorf("expected 1 closed issue, got %d", len(results))
}
return nil
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
}
// TestTransactionSearchIssuesReadYourWrites is the KEY test: create an issue and search
// for it within the same transaction (read-your-writes consistency).
func TestTransactionSearchIssuesReadYourWrites(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
// Create an existing issue outside the transaction
existingIssue := &types.Issue{
Title: "Existing Issue",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
}
if err := store.CreateIssue(ctx, existingIssue, "test-actor"); err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
var newIssueID string
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
// Create a new issue within the transaction
newIssue := &types.Issue{
Title: "Unique Searchable Title XYZ123",
Description: "This has special content ABC789",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeFeature,
}
if err := tx.CreateIssue(ctx, newIssue, "test-actor"); err != nil {
return err
}
newIssueID = newIssue.ID
// CRITICAL: Search for the just-created issue by title
results, err := tx.SearchIssues(ctx, "XYZ123", types.IssueFilter{})
if err != nil {
return err
}
if len(results) != 1 {
t.Errorf("read-your-writes FAILED: expected 1 issue with title 'XYZ123', got %d", len(results))
return nil
}
if results[0].ID != newIssueID {
t.Errorf("read-your-writes FAILED: found wrong issue, expected %s, got %s", newIssueID, results[0].ID)
}
// Search for it by description
results, err = tx.SearchIssues(ctx, "ABC789", types.IssueFilter{})
if err != nil {
return err
}
if len(results) != 1 {
t.Errorf("read-your-writes FAILED: expected 1 issue with description 'ABC789', got %d", len(results))
}
// Search by type filter
featureType := types.TypeFeature
results, err = tx.SearchIssues(ctx, "", types.IssueFilter{IssueType: &featureType})
if err != nil {
return err
}
if len(results) != 1 {
t.Errorf("read-your-writes FAILED: expected 1 feature type issue, got %d", len(results))
}
return nil
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
// Verify the issue was committed
issue, err := store.GetIssue(ctx, newIssueID)
if err != nil {
t.Fatalf("GetIssue failed: %v", err)
}
if issue == nil {
t.Error("expected issue to be committed, but it wasn't found")
}
}
// TestTransactionSearchIssuesWithFilters tests various filter options within transaction.
func TestTransactionSearchIssuesWithFilters(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
closedAt := time.Now()
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
// Create issues with different attributes
issues := []*types.Issue{
{Title: "P1 Bug", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeBug, Assignee: "alice"},
{Title: "P2 Task", Status: types.StatusInProgress, Priority: 2, IssueType: types.TypeTask, Assignee: "bob"},
{Title: "P3 Feature", Status: types.StatusClosed, Priority: 3, IssueType: types.TypeFeature, Assignee: "alice", ClosedAt: &closedAt},
}
for _, issue := range issues {
if err := tx.CreateIssue(ctx, issue, "test-actor"); err != nil {
return err
}
}
// Filter by assignee
assignee := "alice"
results, err := tx.SearchIssues(ctx, "", types.IssueFilter{Assignee: &assignee})
if err != nil {
return err
}
if len(results) != 2 {
t.Errorf("expected 2 issues assigned to alice, got %d", len(results))
}
// Filter by priority
priority := 1
results, err = tx.SearchIssues(ctx, "", types.IssueFilter{Priority: &priority})
if err != nil {
return err
}
if len(results) != 1 {
t.Errorf("expected 1 P1 issue, got %d", len(results))
}
// Filter by type
bugType := types.TypeBug
results, err = tx.SearchIssues(ctx, "", types.IssueFilter{IssueType: &bugType})
if err != nil {
return err
}
if len(results) != 1 {
t.Errorf("expected 1 bug, got %d", len(results))
}
// Combined filter: status + assignee
inProgressStatus := types.StatusInProgress
bobAssignee := "bob"
results, err = tx.SearchIssues(ctx, "", types.IssueFilter{
Status: &inProgressStatus,
Assignee: &bobAssignee,
})
if err != nil {
return err
}
if len(results) != 1 {
t.Errorf("expected 1 in_progress issue assigned to bob, got %d", len(results))
}
return nil
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
}
// TestTransactionSearchIssuesWithLabels tests label filtering within transaction.
func TestTransactionSearchIssuesWithLabels(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
// Create issues
issue1 := &types.Issue{Title: "Issue 1", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
issue2 := &types.Issue{Title: "Issue 2", Status: types.StatusOpen, Priority: 2, IssueType: types.TypeTask}
if err := tx.CreateIssue(ctx, issue1, "test-actor"); err != nil {
return err
}
if err := tx.CreateIssue(ctx, issue2, "test-actor"); err != nil {
return err
}
// Add labels
if err := tx.AddLabel(ctx, issue1.ID, "frontend", "test-actor"); err != nil {
return err
}
if err := tx.AddLabel(ctx, issue1.ID, "urgent", "test-actor"); err != nil {
return err
}
if err := tx.AddLabel(ctx, issue2.ID, "backend", "test-actor"); err != nil {
return err
}
// Search by label (must have ALL labels)
results, err := tx.SearchIssues(ctx, "", types.IssueFilter{Labels: []string{"frontend"}})
if err != nil {
return err
}
if len(results) != 1 {
t.Errorf("expected 1 issue with 'frontend' label, got %d", len(results))
}
if len(results) > 0 && results[0].ID != issue1.ID {
t.Errorf("expected issue1, got %s", results[0].ID)
}
// Verify labels are attached to the issue
if len(results) > 0 && len(results[0].Labels) != 2 {
t.Errorf("expected 2 labels on issue, got %d", len(results[0].Labels))
}
// Search by multiple labels (AND)
results, err = tx.SearchIssues(ctx, "", types.IssueFilter{Labels: []string{"frontend", "urgent"}})
if err != nil {
return err
}
if len(results) != 1 {
t.Errorf("expected 1 issue with both 'frontend' and 'urgent' labels, got %d", len(results))
}
// Search by any label (OR)
results, err = tx.SearchIssues(ctx, "", types.IssueFilter{LabelsAny: []string{"frontend", "backend"}})
if err != nil {
return err
}
if len(results) != 2 {
t.Errorf("expected 2 issues with either 'frontend' or 'backend' label, got %d", len(results))
}
return nil
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
}
// TestTransactionSearchIssuesRollback verifies uncommitted issues aren't visible outside transaction.
func TestTransactionSearchIssuesRollback(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
// Try to create an issue but rollback (by returning an error)
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
issue := &types.Issue{
Title: "RollbackTestIssue999",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
}
if err := tx.CreateIssue(ctx, issue, "test-actor"); err != nil {
return err
}
// Verify it's visible within the transaction
results, err := tx.SearchIssues(ctx, "RollbackTestIssue999", types.IssueFilter{})
if err != nil {
return err
}
if len(results) != 1 {
t.Errorf("expected issue to be visible within transaction, got %d results", len(results))
}
// Return error to trigger rollback
return &testError{msg: "intentional rollback"}
})
if err == nil {
t.Fatal("expected error from rollback, got nil")
}
// Verify the issue is NOT visible outside the transaction (it was rolled back)
results, err := store.SearchIssues(ctx, "RollbackTestIssue999", types.IssueFilter{})
if err != nil {
t.Fatalf("SearchIssues failed: %v", err)
}
if len(results) != 0 {
t.Errorf("expected 0 issues after rollback, got %d - rollback didn't work!", len(results))
}
}
// TestTransactionSearchIssuesLimit tests the limit filter within transaction.
func TestTransactionSearchIssuesLimit(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
// Create several issues
for i := 0; i < 10; i++ {
issue := &types.Issue{
Title: "Limit Test Issue",
Status: types.StatusOpen,
Priority: i % 5,
IssueType: types.TypeTask,
}
if err := tx.CreateIssue(ctx, issue, "test-actor"); err != nil {
return err
}
}
// Search with limit
results, err := tx.SearchIssues(ctx, "", types.IssueFilter{Limit: 3})
if err != nil {
return err
}
if len(results) != 3 {
t.Errorf("expected 3 issues with limit, got %d", len(results))
}
// Search without limit should return all
results, err = tx.SearchIssues(ctx, "", types.IssueFilter{})
if err != nil {
return err
}
if len(results) != 10 {
t.Errorf("expected 10 issues without limit, got %d", len(results))
}
return nil
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
}
// TestTransactionSearchIssuesWithPriorityRange tests priority range filters within transaction.
func TestTransactionSearchIssuesWithPriorityRange(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
// Create issues with different priorities
for i := 0; i < 5; i++ {
issue := &types.Issue{
Title: "Priority Range Test",
Status: types.StatusOpen,
Priority: i, // P0, P1, P2, P3, P4
IssueType: types.TypeTask,
}
if err := tx.CreateIssue(ctx, issue, "test-actor"); err != nil {
return err
}
}
// Filter by PriorityMin only (P2 and higher priority = lower number)
minPriority := 2
results, err := tx.SearchIssues(ctx, "", types.IssueFilter{PriorityMin: &minPriority})
if err != nil {
return err
}
// Should get P2, P3, P4
if len(results) != 3 {
t.Errorf("expected 3 issues with priority >= 2, got %d", len(results))
}
// Filter by PriorityMax only
maxPriority := 1
results, err = tx.SearchIssues(ctx, "", types.IssueFilter{PriorityMax: &maxPriority})
if err != nil {
return err
}
// Should get P0, P1
if len(results) != 2 {
t.Errorf("expected 2 issues with priority <= 1, got %d", len(results))
}
// Filter by priority range
minP := 1
maxP := 3
results, err = tx.SearchIssues(ctx, "", types.IssueFilter{PriorityMin: &minP, PriorityMax: &maxP})
if err != nil {
return err
}
// Should get P1, P2, P3
if len(results) != 3 {
t.Errorf("expected 3 issues with priority 1-3, got %d", len(results))
}
return nil
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
}
// TestTransactionSearchIssuesWithDateRange tests date range filters within transaction.
func TestTransactionSearchIssuesWithDateRange(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
now := time.Now()
past := now.Add(-48 * time.Hour)
future := now.Add(24 * time.Hour)
// Create issues - CreatedAt is set automatically
issue1 := &types.Issue{
Title: "Recent Issue",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
}
if err := tx.CreateIssue(ctx, issue1, "test-actor"); err != nil {
return err
}
// Filter by CreatedAfter (should find the issue created just now)
createdAfter := past
results, err := tx.SearchIssues(ctx, "", types.IssueFilter{CreatedAfter: &createdAfter})
if err != nil {
return err
}
if len(results) != 1 {
t.Errorf("expected 1 issue created after past, got %d", len(results))
}
// Filter by CreatedBefore with future time (should find the issue)
results, err = tx.SearchIssues(ctx, "", types.IssueFilter{CreatedBefore: &future})
if err != nil {
return err
}
if len(results) != 1 {
t.Errorf("expected 1 issue created before future, got %d", len(results))
}
// Filter by CreatedBefore with past time (should find nothing)
results, err = tx.SearchIssues(ctx, "", types.IssueFilter{CreatedBefore: &past})
if err != nil {
return err
}
if len(results) != 0 {
t.Errorf("expected 0 issues created before past, got %d", len(results))
}
return nil
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
}
// TestTransactionSearchIssuesWithIDs tests IDs filter within transaction.
func TestTransactionSearchIssuesWithIDs(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
// Create several issues and collect their IDs
var issueIDs []string
for i := 0; i < 5; i++ {
issue := &types.Issue{
Title: "IDs Filter Test",
Status: types.StatusOpen,
Priority: i,
IssueType: types.TypeTask,
}
if err := tx.CreateIssue(ctx, issue, "test-actor"); err != nil {
return err
}
issueIDs = append(issueIDs, issue.ID)
}
// Filter by specific IDs (first 2)
results, err := tx.SearchIssues(ctx, "", types.IssueFilter{IDs: issueIDs[:2]})
if err != nil {
return err
}
if len(results) != 2 {
t.Errorf("expected 2 issues when filtering by 2 IDs, got %d", len(results))
}
// Filter by single ID
results, err = tx.SearchIssues(ctx, "", types.IssueFilter{IDs: []string{issueIDs[0]}})
if err != nil {
return err
}
if len(results) != 1 {
t.Errorf("expected 1 issue when filtering by 1 ID, got %d", len(results))
}
if results[0].ID != issueIDs[0] {
t.Errorf("expected issue ID %s, got %s", issueIDs[0], results[0].ID)
}
// Filter by non-existent ID
results, err = tx.SearchIssues(ctx, "", types.IssueFilter{IDs: []string{"nonexistent-id"}})
if err != nil {
return err
}
if len(results) != 0 {
t.Errorf("expected 0 issues for non-existent ID, got %d", len(results))
}
// Empty IDs filter should return all issues
results, err = tx.SearchIssues(ctx, "", types.IssueFilter{IDs: []string{}})
if err != nil {
return err
}
if len(results) != 5 {
t.Errorf("expected 5 issues with empty IDs filter, got %d", len(results))
}
return nil
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
}