Files
kandev/apps/backend/internal/db/migration_errors_test.go
T
Henry 4c0d92154b feat(backend): add per-run cost/token ledger for task usage (#2960)
Co-authored-by: nova28 <17953305+nova28@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
2026-08-23 22:15:19 +00:00

322 lines
7.2 KiB
Go

package db
import (
"errors"
"fmt"
"testing"
"github.com/jackc/pgx/v5/pgconn"
)
func TestIsDuplicateColumnError(t *testing.T) {
tests := []struct {
name string
err error
want bool
}{
{
name: "sqlite duplicate column",
err: errors.New("duplicate column name: branch_slug"),
want: true,
},
{
name: "postgres duplicate column",
err: &pgconn.PgError{
Code: postgresDuplicateColumn,
Message: `column "branch_slug" of relation "task_session_worktrees" already exists`,
},
want: true,
},
{
name: "wrapped postgres duplicate column",
err: fmt.Errorf("add column: %w", &pgconn.PgError{
Code: postgresDuplicateColumn,
Message: `column "branch_slug" of relation "task_session_worktrees" already exists`,
}),
want: true,
},
{
name: "postgres duplicate table is not a duplicate column",
err: &pgconn.PgError{
Code: postgresDuplicateTable,
Message: `relation "task_session_worktrees" already exists`,
},
want: false,
},
{
name: "unrelated",
err: errors.New("no such table: task_session_worktrees"),
want: false,
},
{
name: "nil",
err: nil,
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IsDuplicateColumnError(tt.err); got != tt.want {
t.Fatalf("IsDuplicateColumnError() = %v, want %v", got, tt.want)
}
})
}
}
func TestIsAlreadyExistsError(t *testing.T) {
tests := []struct {
name string
err error
want bool
}{
{
name: "sqlite duplicate column",
err: errors.New("duplicate column name: branch_slug"),
want: true,
},
{
name: "sqlite already exists",
err: errors.New("index idx_task_session_worktrees_status already exists"),
want: true,
},
{
name: "sqlite table already exists",
err: errors.New("table task_session_worktrees already exists"),
want: true,
},
{
name: "sqlite unrelated already exists text",
err: errors.New("migration failed because a dependency already exists in an invalid state"),
want: false,
},
{
name: "postgres duplicate column",
err: &pgconn.PgError{Code: postgresDuplicateColumn},
want: true,
},
{
name: "wrapped postgres duplicate column",
err: fmt.Errorf("add column: %w", &pgconn.PgError{Code: postgresDuplicateColumn}),
want: true,
},
{
name: "postgres duplicate table",
err: &pgconn.PgError{Code: postgresDuplicateTable},
want: true,
},
{
name: "postgres duplicate object",
err: &pgconn.PgError{Code: postgresDuplicateObject},
want: true,
},
{
name: "postgres undefined column",
err: &pgconn.PgError{Code: "42703"},
want: false,
},
{
name: "postgres non-duplicate code ignores broad message text",
err: &pgconn.PgError{
Code: "42703",
Message: `relation "task_session_worktrees" already exists`,
},
want: false,
},
{
name: "unrelated",
err: errors.New("no such table: task_session_worktrees"),
want: false,
},
{
name: "nil",
err: nil,
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IsAlreadyExistsError(tt.err); got != tt.want {
t.Fatalf("IsAlreadyExistsError() = %v, want %v", got, tt.want)
}
})
}
}
func TestIsMissingTableError(t *testing.T) {
tests := []struct {
name string
err error
want bool
}{
{
name: "sqlite no such table",
err: errors.New("no such table: tasks"),
want: true,
},
{
name: "wrapped sqlite no such table",
err: fmt.Errorf("count active runs: %w", errors.New("no such table: tasks")),
want: true,
},
{
name: "postgres undefined table",
err: &pgconn.PgError{Code: postgresUndefinedTable},
want: true,
},
{
name: "wrapped postgres undefined table",
err: fmt.Errorf("count active runs: %w", &pgconn.PgError{Code: postgresUndefinedTable}),
want: true,
},
{
name: "postgres undefined column is not a missing table",
err: &pgconn.PgError{Code: "42703"},
want: false,
},
{
name: "unrelated",
err: errors.New("duplicate column name: branch_slug"),
want: false,
},
{
name: "nil",
err: nil,
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IsMissingTableError(tt.err); got != tt.want {
t.Fatalf("IsMissingTableError() = %v, want %v", got, tt.want)
}
})
}
}
func TestIsForeignKeyViolation(t *testing.T) {
tests := []struct {
name string
err error
want bool
}{
{
name: "postgres foreign key violation",
err: &pgconn.PgError{Code: postgresForeignKeyViolation},
want: true,
},
{
name: "wrapped postgres foreign key violation",
err: fmt.Errorf("insert usage event: %w", &pgconn.PgError{Code: postgresForeignKeyViolation}),
want: true,
},
{
name: "postgres unique violation is not a foreign key violation",
err: &pgconn.PgError{Code: "23505"},
want: false,
},
{
name: "sqlite foreign key constraint failed",
err: errors.New("FOREIGN KEY constraint failed"),
want: true,
},
{
name: "wrapped sqlite foreign key constraint failed",
err: fmt.Errorf("insert usage event: %w", errors.New("FOREIGN KEY constraint failed")),
want: true,
},
{
name: "sqlite unique constraint is not a foreign key violation",
err: errors.New("UNIQUE constraint failed: task_usage_events.usage_event_id"),
want: false,
},
{
name: "unrelated",
err: errors.New("no such table: task_usage_events"),
want: false,
},
{
name: "nil",
err: nil,
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IsForeignKeyViolation(tt.err); got != tt.want {
t.Fatalf("IsForeignKeyViolation() = %v, want %v", got, tt.want)
}
})
}
}
func TestIsTransientError(t *testing.T) {
tests := []struct {
name string
err error
want bool
}{
{
name: "postgres serialization failure",
err: &pgconn.PgError{Code: postgresSerializationFailure},
want: true,
},
{
name: "postgres deadlock detected",
err: &pgconn.PgError{Code: postgresDeadlockDetected},
want: true,
},
{
name: "wrapped postgres serialization failure",
err: fmt.Errorf("insert usage event: %w", &pgconn.PgError{Code: postgresSerializationFailure}),
want: true,
},
{
name: "postgres foreign key violation is not transient",
err: &pgconn.PgError{Code: postgresForeignKeyViolation},
want: false,
},
{
name: "sqlite busy",
err: errors.New("database is locked"),
want: true,
},
{
name: "sqlite locked",
err: errors.New("database table is locked"),
want: true,
},
{
name: "wrapped sqlite busy",
err: fmt.Errorf("insert usage event: %w", errors.New("database is locked")),
want: true,
},
{
name: "sqlite constraint error is not transient",
err: errors.New("UNIQUE constraint failed: task_usage_events.usage_event_id"),
want: false,
},
{
name: "unrelated",
err: errors.New("no such table: task_usage_events"),
want: false,
},
{
name: "nil",
err: nil,
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IsTransientError(tt.err); got != tt.want {
t.Fatalf("IsTransientError() = %v, want %v", got, tt.want)
}
})
}
}