mirror of
https://github.com/lukaszraczylo/claude-mnemonic.git
synced 2026-06-08 23:39:40 +00:00
7a061c85eb
* refactor(hooks): simplify hook execution with shared context - [x] Extract BaseInput struct to eliminate duplicate fields across hooks - [x] Create RunHook handler pattern for session-start and user-prompt - [x] Create RunStatuslineHook for fast statusline rendering without worker startup - [x] Add HookContext struct to pass port, project, CWD, SessionID to handlers - [x] Add db/interface.go with ObservationReader/Writer interfaces - [x] Add comprehensive conflict management tests in sqlite/conflict_test.go - [x] Add vector client tests for Count, ModelVersion, NeedsRebuild, GetStaleVectors - [x] Add FilterByThreshold helper tests for query result filtering - [x] Make handlers_test more robust for network-dependent update checks - [x] Update package versions in UI * Move to GORM + general cleanup * feat(mcp): add observation relations discovery and scoring integration - [x] Add find_related_observations MCP tool for discovering related observations by confidence - [x] Integrate scoring calculator and recalculator into MCP server initialization - [x] Add pattern, relation, and session stores to MCP server dependencies - [x] Register MCP server in Claude Code settings during plugin installation - [x] Update install scripts (bash, PowerShell) to configure MCP server settings - [x] Switch plugin manifest files to template-based versioning (plugin.json.tpl, marketplace.json.tpl) - [x] Update all MCP server tests to pass new dependency parameters
344 lines
9.6 KiB
Go
344 lines
9.6 KiB
Go
//go:build fts5
|
|
|
|
// Package gorm provides GORM-based database operations for claude-mnemonic.
|
|
package gorm
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/gorm/logger"
|
|
|
|
"github.com/lukaszraczylo/claude-mnemonic/pkg/models"
|
|
)
|
|
|
|
// TestIntegration_EndToEndWorkflow verifies a complete workflow
|
|
// simulating real usage of the GORM package.
|
|
func TestIntegration_EndToEndWorkflow(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "gorm_integration_test_*")
|
|
require.NoError(t, err)
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
dbPath := filepath.Join(tmpDir, "test.db")
|
|
cfg := Config{
|
|
Path: dbPath,
|
|
MaxConns: 4,
|
|
LogLevel: logger.Silent,
|
|
}
|
|
|
|
// Step 1: Initialize store
|
|
store, err := NewStore(cfg)
|
|
require.NoError(t, err)
|
|
defer store.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Step 2: Create all store types
|
|
sessionStore := NewSessionStore(store)
|
|
summaryStore := NewSummaryStore(store)
|
|
conflictStore := NewConflictStore(store)
|
|
relationStore := NewRelationStore(store)
|
|
patternStore := NewPatternStore(store)
|
|
|
|
// Create observation store with dependencies
|
|
observationStore := NewObservationStore(store, nil, conflictStore, relationStore)
|
|
promptStore := NewPromptStore(store, nil)
|
|
|
|
// Step 3: Create a session
|
|
sessionID, err := sessionStore.CreateSDKSession(ctx, "claude-test", "test-project", "")
|
|
require.NoError(t, err)
|
|
assert.Greater(t, sessionID, int64(0))
|
|
|
|
// Step 4: Store observations
|
|
obs1 := &models.ParsedObservation{
|
|
Type: models.ObsTypeDiscovery,
|
|
Title: "Test Discovery",
|
|
Subtitle: "Testing GORM integration",
|
|
Facts: []string{"Fact 1", "Fact 2"},
|
|
Concepts: []string{"testing", "integration"},
|
|
}
|
|
|
|
obsID1, _, err := observationStore.StoreObservation(ctx, "claude-test", "test-project", obs1, int(sessionID), 1)
|
|
require.NoError(t, err)
|
|
assert.Greater(t, obsID1, int64(0))
|
|
|
|
obs2 := &models.ParsedObservation{
|
|
Type: models.ObsTypeBugfix,
|
|
Title: "Test Bugfix",
|
|
Facts: []string{"Fixed bug"},
|
|
Concepts: []string{"bugfix"},
|
|
}
|
|
|
|
obsID2, _, err := observationStore.StoreObservation(ctx, "claude-test", "test-project", obs2, int(sessionID), 2)
|
|
require.NoError(t, err)
|
|
assert.Greater(t, obsID2, int64(0))
|
|
|
|
// Step 5: Create relations
|
|
now := time.Now()
|
|
relation := &models.ObservationRelation{
|
|
SourceID: obsID1,
|
|
TargetID: obsID2,
|
|
RelationType: models.RelationCauses,
|
|
Confidence: 0.8,
|
|
DetectionSource: models.DetectionSourceFileOverlap,
|
|
CreatedAt: now.Format(time.RFC3339),
|
|
CreatedAtEpoch: now.UnixMilli(),
|
|
}
|
|
|
|
relID, err := relationStore.StoreRelation(ctx, relation)
|
|
require.NoError(t, err)
|
|
assert.Greater(t, relID, int64(0))
|
|
|
|
// Step 6: Update importance scores
|
|
err = observationStore.UpdateImportanceScore(ctx, obsID1, 5.0)
|
|
require.NoError(t, err)
|
|
|
|
// Step 7: Increment retrieval counts
|
|
err = observationStore.IncrementRetrievalCount(ctx, []int64{obsID1, obsID2})
|
|
require.NoError(t, err)
|
|
|
|
// Step 8: Create a pattern
|
|
pattern := &models.Pattern{
|
|
Name: "Test Pattern",
|
|
Type: models.PatternTypeBug,
|
|
Signature: []string{"bug", "fix"},
|
|
Frequency: 1,
|
|
Projects: []string{"test-project"},
|
|
ObservationIDs: []int64{obsID1, obsID2},
|
|
Status: models.PatternStatusActive,
|
|
Confidence: 0.75,
|
|
LastSeenAt: now.Format(time.RFC3339),
|
|
LastSeenEpoch: now.UnixMilli(),
|
|
CreatedAt: now.Format(time.RFC3339),
|
|
CreatedAtEpoch: now.UnixMilli(),
|
|
}
|
|
|
|
patternID, err := patternStore.StorePattern(ctx, pattern)
|
|
require.NoError(t, err)
|
|
assert.Greater(t, patternID, int64(0))
|
|
|
|
// Step 9: Store a prompt
|
|
promptID, err := promptStore.SaveUserPromptWithMatches(ctx, "claude-test", 1, "Test prompt", 2)
|
|
require.NoError(t, err)
|
|
assert.Greater(t, promptID, int64(0))
|
|
|
|
// Step 10: Store a summary
|
|
summary := &models.ParsedSummary{
|
|
Request: "Test request",
|
|
Investigated: "Test investigation",
|
|
Learned: "Test learning",
|
|
Completed: "Test completion",
|
|
NextSteps: "Test next steps",
|
|
Notes: "Test notes",
|
|
}
|
|
|
|
summaryID, _, err := summaryStore.StoreSummary(ctx, "claude-test", "test-project", summary, 1, 100)
|
|
require.NoError(t, err)
|
|
assert.Greater(t, summaryID, int64(0))
|
|
|
|
// Step 11: Verify data retrieval
|
|
retrievedObs, err := observationStore.GetObservationByID(ctx, obsID1)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, retrievedObs)
|
|
assert.Equal(t, "Test Discovery", retrievedObs.Title.String)
|
|
assert.Equal(t, 5.0, retrievedObs.ImportanceScore)
|
|
assert.Equal(t, 1, retrievedObs.RetrievalCount)
|
|
|
|
// Step 12: Verify relations
|
|
relations, err := relationStore.GetRelationsByObservationID(ctx, obsID1)
|
|
require.NoError(t, err)
|
|
assert.Len(t, relations, 1)
|
|
assert.Equal(t, obsID2, relations[0].TargetID)
|
|
|
|
// Step 13: Verify pattern
|
|
retrievedPattern, err := patternStore.GetPatternByID(ctx, patternID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, retrievedPattern)
|
|
assert.Equal(t, "Test Pattern", retrievedPattern.Name)
|
|
|
|
// Step 14: Verify stats
|
|
stats, err := observationStore.GetObservationFeedbackStats(ctx, "test-project")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 2, stats.Total)
|
|
|
|
t.Log("✅ End-to-end integration test passed!")
|
|
}
|
|
|
|
// TestIntegration_StoreCompatibility verifies that Store methods work correctly.
|
|
func TestIntegration_StoreCompatibility(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "gorm_store_test_*")
|
|
require.NoError(t, err)
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
dbPath := filepath.Join(tmpDir, "test.db")
|
|
cfg := Config{
|
|
Path: dbPath,
|
|
MaxConns: 4,
|
|
LogLevel: logger.Silent,
|
|
}
|
|
|
|
store, err := NewStore(cfg)
|
|
require.NoError(t, err)
|
|
defer store.Close()
|
|
|
|
// Verify raw DB access (needed for vector client)
|
|
rawDB := store.GetRawDB()
|
|
require.NotNil(t, rawDB)
|
|
assert.IsType(t, &sql.DB{}, rawDB)
|
|
|
|
// Verify GORM DB access
|
|
gormDB := store.GetDB()
|
|
require.NotNil(t, gormDB)
|
|
|
|
// Verify Close works
|
|
err = store.Close()
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// TestIntegration_ConcurrentAccess verifies thread-safe operations.
|
|
func TestIntegration_ConcurrentAccess(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "gorm_concurrent_test_*")
|
|
require.NoError(t, err)
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
dbPath := filepath.Join(tmpDir, "test.db")
|
|
cfg := Config{
|
|
Path: dbPath,
|
|
MaxConns: 4,
|
|
LogLevel: logger.Silent,
|
|
}
|
|
|
|
store, err := NewStore(cfg)
|
|
require.NoError(t, err)
|
|
defer store.Close()
|
|
|
|
sessionStore := NewSessionStore(store)
|
|
ctx := context.Background()
|
|
|
|
// Create session
|
|
sessionID, err := sessionStore.CreateSDKSession(ctx, "claude-concurrent", "test-project", "")
|
|
require.NoError(t, err)
|
|
|
|
// Concurrent prompt counter increments
|
|
done := make(chan bool)
|
|
numGoroutines := 10
|
|
|
|
for i := 0; i < numGoroutines; i++ {
|
|
go func() {
|
|
_, err := sessionStore.IncrementPromptCounter(ctx, sessionID)
|
|
assert.NoError(t, err)
|
|
done <- true
|
|
}()
|
|
}
|
|
|
|
// Wait for all goroutines
|
|
for i := 0; i < numGoroutines; i++ {
|
|
<-done
|
|
}
|
|
|
|
// Verify final count
|
|
session, err := sessionStore.GetSessionByID(ctx, sessionID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(numGoroutines), int64(session.PromptCounter))
|
|
|
|
t.Log("✅ Concurrent access test passed!")
|
|
}
|
|
|
|
// TestIntegration_WALMode verifies WAL mode is enabled.
|
|
func TestIntegration_WALMode(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "gorm_wal_test_*")
|
|
require.NoError(t, err)
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
dbPath := filepath.Join(tmpDir, "test.db")
|
|
cfg := Config{
|
|
Path: dbPath,
|
|
MaxConns: 4,
|
|
LogLevel: logger.Silent,
|
|
}
|
|
|
|
store, err := NewStore(cfg)
|
|
require.NoError(t, err)
|
|
defer store.Close()
|
|
|
|
// Check WAL mode via raw SQL
|
|
var journalMode string
|
|
err = store.GetRawDB().QueryRow("PRAGMA journal_mode").Scan(&journalMode)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "wal", journalMode, "WAL mode should be enabled")
|
|
|
|
t.Log("✅ WAL mode verification passed!")
|
|
}
|
|
|
|
// TestIntegration_FTS5Search verifies FTS5 functionality.
|
|
func TestIntegration_FTS5Search(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "gorm_fts5_test_*")
|
|
require.NoError(t, err)
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
dbPath := filepath.Join(tmpDir, "test.db")
|
|
cfg := Config{
|
|
Path: dbPath,
|
|
MaxConns: 4,
|
|
LogLevel: logger.Silent,
|
|
}
|
|
|
|
store, err := NewStore(cfg)
|
|
require.NoError(t, err)
|
|
defer store.Close()
|
|
|
|
ctx := context.Background()
|
|
sessionStore := NewSessionStore(store)
|
|
observationStore := NewObservationStore(store, nil, nil, nil)
|
|
|
|
// Create session
|
|
sessionID, _ := sessionStore.CreateSDKSession(ctx, "claude-fts5", "test-project", "")
|
|
|
|
// Store observations with searchable text
|
|
obs1 := &models.ParsedObservation{
|
|
Type: models.ObsTypeDiscovery,
|
|
Title: "Database optimization techniques",
|
|
Subtitle: "Improving query performance",
|
|
Facts: []string{"Use indexes", "Optimize queries"},
|
|
Concepts: []string{"performance", "optimization"},
|
|
}
|
|
|
|
obsID1, _, _ := observationStore.StoreObservation(ctx, "claude-fts5", "test-project", obs1, int(sessionID), 1)
|
|
|
|
obs2 := &models.ParsedObservation{
|
|
Type: models.ObsTypeBugfix,
|
|
Title: "Fixed memory leak",
|
|
Facts: []string{"Closed connections properly"},
|
|
Concepts: []string{"bugfix", "memory"},
|
|
}
|
|
|
|
observationStore.StoreObservation(ctx, "claude-fts5", "test-project", obs2, int(sessionID), 2)
|
|
|
|
// Give FTS5 triggers time to process
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Search using FTS5
|
|
results, err := observationStore.SearchObservationsFTS(ctx, "optimization", "test-project", 10)
|
|
require.NoError(t, err)
|
|
|
|
// Should find the optimization observation
|
|
assert.NotEmpty(t, results, "FTS5 search should return results")
|
|
|
|
found := false
|
|
for _, obs := range results {
|
|
if obs.ID == obsID1 {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found, "FTS5 should find the optimization observation")
|
|
|
|
t.Log("✅ FTS5 search test passed!")
|
|
}
|