mirror of
https://github.com/lukaszraczylo/claude-mnemonic.git
synced 2026-06-05 23:03:55 +00:00
530 lines
11 KiB
Go
530 lines
11 KiB
Go
// Package sqlite provides SQLite database operations for claude-mnemonic.
|
|
package sqlite
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/stretchr/testify/suite"
|
|
)
|
|
|
|
// StoreSuite is a test suite for Store operations.
|
|
type StoreSuite struct {
|
|
suite.Suite
|
|
db *sql.DB
|
|
store *Store
|
|
cleanup func()
|
|
}
|
|
|
|
// SetupTest creates a fresh database before each test.
|
|
func (s *StoreSuite) SetupTest() {
|
|
s.db, _, s.cleanup = testDB(s.T())
|
|
createBaseTables(s.T(), s.db)
|
|
s.store = newStoreFromDB(s.db)
|
|
}
|
|
|
|
// TearDownTest cleans up after each test.
|
|
func (s *StoreSuite) TearDownTest() {
|
|
if s.cleanup != nil {
|
|
s.cleanup()
|
|
}
|
|
}
|
|
|
|
func TestStoreSuite(t *testing.T) {
|
|
suite.Run(t, new(StoreSuite))
|
|
}
|
|
|
|
// TestGetStmt tests prepared statement caching.
|
|
func (s *StoreSuite) TestGetStmt() {
|
|
tests := []struct {
|
|
name string
|
|
query string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "valid simple query",
|
|
query: "SELECT 1",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "valid query with parameter",
|
|
query: "SELECT * FROM sdk_sessions WHERE id = ?",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "invalid query syntax",
|
|
query: "SELECT * FROM nonexistent_table WHERE",
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
s.Run(tt.name, func() {
|
|
stmt, err := s.store.GetStmt(tt.query)
|
|
if tt.wantErr {
|
|
s.Error(err)
|
|
s.Nil(stmt)
|
|
} else {
|
|
s.NoError(err)
|
|
s.NotNil(stmt)
|
|
|
|
// Second call should return cached statement
|
|
stmt2, err := s.store.GetStmt(tt.query)
|
|
s.NoError(err)
|
|
s.Same(stmt, stmt2)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestExecContext tests query execution.
|
|
func (s *StoreSuite) TestExecContext() {
|
|
ctx := context.Background()
|
|
|
|
tests := []struct {
|
|
name string
|
|
query string
|
|
args []interface{}
|
|
wantErr bool
|
|
wantAffected int64
|
|
}{
|
|
{
|
|
name: "insert session",
|
|
query: `INSERT INTO sdk_sessions (claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
|
VALUES (?, ?, ?, datetime('now'), strftime('%s', 'now') * 1000, 'active')`,
|
|
args: []interface{}{"claude-1", "sdk-1", "test-project"},
|
|
wantErr: false,
|
|
wantAffected: 1,
|
|
},
|
|
{
|
|
name: "invalid query",
|
|
query: "INSERT INTO nonexistent_table VALUES (?)",
|
|
args: []interface{}{"test"},
|
|
wantErr: true,
|
|
wantAffected: 0,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
s.Run(tt.name, func() {
|
|
result, err := s.store.ExecContext(ctx, tt.query, tt.args...)
|
|
if tt.wantErr {
|
|
s.Error(err)
|
|
} else {
|
|
s.NoError(err)
|
|
affected, _ := result.RowsAffected()
|
|
s.Equal(tt.wantAffected, affected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestQueryContext tests query execution that returns rows.
|
|
func (s *StoreSuite) TestQueryContext() {
|
|
ctx := context.Background()
|
|
|
|
// Seed data
|
|
seedSession(s.T(), s.db, "claude-1", "sdk-1", "project-a")
|
|
|
|
tests := []struct {
|
|
name string
|
|
query string
|
|
args []interface{}
|
|
wantErr bool
|
|
wantRows int
|
|
setupFunc func()
|
|
assertFunc func(rows *sql.Rows)
|
|
}{
|
|
{
|
|
name: "query existing session",
|
|
query: "SELECT id, project FROM sdk_sessions WHERE claude_session_id = ?",
|
|
args: []interface{}{"claude-1"},
|
|
wantErr: false,
|
|
wantRows: 1,
|
|
},
|
|
{
|
|
name: "query non-existent session",
|
|
query: "SELECT id, project FROM sdk_sessions WHERE claude_session_id = ?",
|
|
args: []interface{}{"nonexistent"},
|
|
wantErr: false,
|
|
wantRows: 0,
|
|
},
|
|
{
|
|
name: "query all sessions",
|
|
query: "SELECT id, project FROM sdk_sessions",
|
|
args: nil,
|
|
wantErr: false,
|
|
wantRows: 1,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
s.Run(tt.name, func() {
|
|
rows, err := s.store.QueryContext(ctx, tt.query, tt.args...)
|
|
if tt.wantErr {
|
|
s.Error(err)
|
|
return
|
|
}
|
|
|
|
s.NoError(err)
|
|
defer rows.Close()
|
|
|
|
count := 0
|
|
for rows.Next() {
|
|
count++
|
|
}
|
|
s.Equal(tt.wantRows, count)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestQueryRowContext tests single row query execution.
|
|
func (s *StoreSuite) TestQueryRowContext() {
|
|
ctx := context.Background()
|
|
|
|
// Seed data
|
|
seedSession(s.T(), s.db, "claude-1", "sdk-1", "project-a")
|
|
|
|
tests := []struct {
|
|
name string
|
|
query string
|
|
args []interface{}
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "query existing session",
|
|
query: "SELECT id FROM sdk_sessions WHERE claude_session_id = ?",
|
|
args: []interface{}{"claude-1"},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "query non-existent session",
|
|
query: "SELECT id FROM sdk_sessions WHERE claude_session_id = ?",
|
|
args: []interface{}{"nonexistent"},
|
|
wantErr: true, // sql.ErrNoRows
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
s.Run(tt.name, func() {
|
|
row := s.store.QueryRowContext(ctx, tt.query, tt.args...)
|
|
var id int64
|
|
err := row.Scan(&id)
|
|
if tt.wantErr {
|
|
s.Error(err)
|
|
} else {
|
|
s.NoError(err)
|
|
s.Greater(id, int64(0))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestPing tests database connection health check.
|
|
func (s *StoreSuite) TestPing() {
|
|
err := s.store.Ping()
|
|
s.NoError(err)
|
|
}
|
|
|
|
// TestDB tests getting the underlying database connection.
|
|
func (s *StoreSuite) TestDB() {
|
|
db := s.store.DB()
|
|
s.NotNil(db)
|
|
s.Same(s.db, db)
|
|
}
|
|
|
|
// TestClose tests closing the store.
|
|
func (s *StoreSuite) TestClose() {
|
|
// Create a separate store for close test
|
|
db, _, cleanup := testDB(s.T())
|
|
defer cleanup()
|
|
|
|
store := newStoreFromDB(db)
|
|
|
|
// Cache a statement first
|
|
_, err := store.GetStmt("SELECT 1")
|
|
s.NoError(err)
|
|
|
|
// Close should not error
|
|
err = store.Close()
|
|
s.NoError(err)
|
|
|
|
// Operations after close should fail
|
|
err = store.Ping()
|
|
s.Error(err)
|
|
}
|
|
|
|
// TestConcurrentStmtCache tests concurrent access to statement cache.
|
|
func (s *StoreSuite) TestConcurrentStmtCache() {
|
|
ctx := context.Background()
|
|
queries := []string{
|
|
"SELECT 1",
|
|
"SELECT 2",
|
|
"SELECT id FROM sdk_sessions",
|
|
"SELECT project FROM sdk_sessions",
|
|
}
|
|
|
|
done := make(chan struct{})
|
|
for i := 0; i < 10; i++ {
|
|
go func(i int) {
|
|
query := queries[i%len(queries)]
|
|
_, _ = s.store.GetStmt(query)
|
|
_, _ = s.store.ExecContext(ctx, "SELECT 1")
|
|
done <- struct{}{}
|
|
}(i)
|
|
}
|
|
|
|
for i := 0; i < 10; i++ {
|
|
<-done
|
|
}
|
|
}
|
|
|
|
// HelpersSuite tests helper functions.
|
|
type HelpersSuite struct {
|
|
suite.Suite
|
|
}
|
|
|
|
func TestHelpersSuite(t *testing.T) {
|
|
suite.Run(t, new(HelpersSuite))
|
|
}
|
|
|
|
func (s *HelpersSuite) TestNullString() {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
wantStr string
|
|
wantBool bool
|
|
}{
|
|
{
|
|
name: "empty string",
|
|
input: "",
|
|
wantStr: "",
|
|
wantBool: false,
|
|
},
|
|
{
|
|
name: "non-empty string",
|
|
input: "test",
|
|
wantStr: "test",
|
|
wantBool: true,
|
|
},
|
|
{
|
|
name: "whitespace string",
|
|
input: " ",
|
|
wantStr: " ",
|
|
wantBool: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
s.Run(tt.name, func() {
|
|
result := nullString(tt.input)
|
|
s.Equal(tt.wantStr, result.String)
|
|
s.Equal(tt.wantBool, result.Valid)
|
|
})
|
|
}
|
|
}
|
|
|
|
func (s *HelpersSuite) TestNullInt() {
|
|
tests := []struct {
|
|
name string
|
|
input int
|
|
wantInt int64
|
|
wantBool bool
|
|
}{
|
|
{
|
|
name: "zero",
|
|
input: 0,
|
|
wantInt: 0,
|
|
wantBool: false,
|
|
},
|
|
{
|
|
name: "negative",
|
|
input: -1,
|
|
wantInt: -1,
|
|
wantBool: false,
|
|
},
|
|
{
|
|
name: "positive",
|
|
input: 42,
|
|
wantInt: 42,
|
|
wantBool: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
s.Run(tt.name, func() {
|
|
result := nullInt(tt.input)
|
|
s.Equal(tt.wantInt, result.Int64)
|
|
s.Equal(tt.wantBool, result.Valid)
|
|
})
|
|
}
|
|
}
|
|
|
|
func (s *HelpersSuite) TestRepeatPlaceholders() {
|
|
tests := []struct {
|
|
name string
|
|
input int
|
|
expected string
|
|
}{
|
|
{
|
|
name: "zero",
|
|
input: 0,
|
|
expected: "",
|
|
},
|
|
{
|
|
name: "negative",
|
|
input: -1,
|
|
expected: "",
|
|
},
|
|
{
|
|
name: "one",
|
|
input: 1,
|
|
expected: ", ?",
|
|
},
|
|
{
|
|
name: "three",
|
|
input: 3,
|
|
expected: ", ?, ?, ?",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
s.Run(tt.name, func() {
|
|
result := repeatPlaceholders(tt.input)
|
|
s.Equal(tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func (s *HelpersSuite) TestInt64SliceToInterface() {
|
|
tests := []struct {
|
|
name string
|
|
input []int64
|
|
expected int
|
|
}{
|
|
{
|
|
name: "empty slice",
|
|
input: []int64{},
|
|
expected: 0,
|
|
},
|
|
{
|
|
name: "single element",
|
|
input: []int64{42},
|
|
expected: 1,
|
|
},
|
|
{
|
|
name: "multiple elements",
|
|
input: []int64{1, 2, 3, 4, 5},
|
|
expected: 5,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
s.Run(tt.name, func() {
|
|
result := int64SliceToInterface(tt.input)
|
|
s.Len(result, tt.expected)
|
|
for i, v := range result {
|
|
s.Equal(tt.input[i], v)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestBuildGetByIDsQuery tests the shared query builder.
|
|
func TestBuildGetByIDsQuery(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
baseQuery string
|
|
ids []int64
|
|
orderBy string
|
|
limit int
|
|
wantQuery string
|
|
wantArgs int
|
|
}{
|
|
{
|
|
name: "single id, no limit, desc order",
|
|
baseQuery: "SELECT * FROM test",
|
|
ids: []int64{1},
|
|
orderBy: "date_desc",
|
|
limit: 0,
|
|
wantQuery: "SELECT * FROM test WHERE id IN (?)\n\t\tORDER BY created_at_epoch DESC",
|
|
wantArgs: 1,
|
|
},
|
|
{
|
|
name: "multiple ids with limit and asc order",
|
|
baseQuery: "SELECT * FROM test",
|
|
ids: []int64{1, 2, 3},
|
|
orderBy: "date_asc",
|
|
limit: 10,
|
|
wantQuery: "SELECT * FROM test WHERE id IN (?, ?, ?)\n\t\tORDER BY created_at_epoch ASC LIMIT ?",
|
|
wantArgs: 4,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
query, args := BuildGetByIDsQuery(tt.baseQuery, tt.ids, tt.orderBy, tt.limit)
|
|
assert.Contains(t, query, "WHERE id IN")
|
|
assert.Len(t, args, tt.wantArgs)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestEnsureSessionExists tests session auto-creation.
|
|
func TestEnsureSessionExists(t *testing.T) {
|
|
db, _, cleanup := testDB(t)
|
|
defer cleanup()
|
|
createBaseTables(t, db)
|
|
|
|
store := newStoreFromDB(db)
|
|
ctx := context.Background()
|
|
|
|
tests := []struct {
|
|
name string
|
|
sdkSessionID string
|
|
project string
|
|
setup func()
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "create new session",
|
|
sdkSessionID: "sdk-new",
|
|
project: "project-a",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "session already exists",
|
|
sdkSessionID: "sdk-existing",
|
|
project: "project-b",
|
|
setup: func() {
|
|
seedSession(t, db, "sdk-existing", "sdk-existing", "project-b")
|
|
},
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if tt.setup != nil {
|
|
tt.setup()
|
|
}
|
|
|
|
err := EnsureSessionExists(ctx, store, tt.sdkSessionID, tt.project)
|
|
if tt.wantErr {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
|
|
// Verify session exists
|
|
var id int64
|
|
err := db.QueryRow("SELECT id FROM sdk_sessions WHERE sdk_session_id = ?", tt.sdkSessionID).Scan(&id)
|
|
require.NoError(t, err)
|
|
assert.Greater(t, id, int64(0))
|
|
}
|
|
})
|
|
}
|
|
}
|