gomcp/internal/application/contextengine/processor_test.go
DmitrL-dev 694e32be26 refactor: rename identity to syntrex, add root orchestration and CI/CD
- Rename Go module: sentinel-community/gomcp -> syntrex/gomcp (50+ files)
- Rename npm package: sentinel-dashboard -> syntrex-dashboard
- Update Cargo.toml repository URL to syntrex/syntrex
- Update all doc references from DmitrL-dev/AISecurity to syntrex
- Add root Makefile (build-all, test-all, lint-all, clean-all)
- Add MIT LICENSE
- Add .editorconfig (Go/Rust/TS/C cross-language)
- Add .github/workflows/ci.yml (Go + Rust + Dashboard)
- Add dashboard next.config.ts and .env.example
- Clean ARCHITECTURE.md: remove brain/immune/strike/micro-swarm, fix 61->67 engines
2026-03-11 15:30:49 +10:00

251 lines
8 KiB
Go

package contextengine
import (
"context"
"testing"
"time"
"github.com/syntrex/gomcp/internal/domain/memory"
"github.com/syntrex/gomcp/internal/infrastructure/sqlite"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// --- mock FactStore for processor tests ---
type procMockFactStore struct {
facts []*memory.Fact
}
func (m *procMockFactStore) Add(_ context.Context, f *memory.Fact) error {
m.facts = append(m.facts, f)
return nil
}
func (m *procMockFactStore) Get(_ context.Context, id string) (*memory.Fact, error) {
for _, f := range m.facts {
if f.ID == id {
return f, nil
}
}
return nil, nil
}
func (m *procMockFactStore) Update(_ context.Context, _ *memory.Fact) error { return nil }
func (m *procMockFactStore) Delete(_ context.Context, _ string) error { return nil }
func (m *procMockFactStore) ListByDomain(_ context.Context, _ string, _ bool) ([]*memory.Fact, error) {
return nil, nil
}
func (m *procMockFactStore) ListByLevel(_ context.Context, _ memory.HierLevel) ([]*memory.Fact, error) {
return nil, nil
}
func (m *procMockFactStore) ListDomains(_ context.Context) ([]string, error) { return nil, nil }
func (m *procMockFactStore) GetStale(_ context.Context, _ bool) ([]*memory.Fact, error) {
return nil, nil
}
func (m *procMockFactStore) Search(_ context.Context, query string, limit int) ([]*memory.Fact, error) {
var results []*memory.Fact
for _, f := range m.facts {
if len(results) >= limit {
break
}
if contains(f.Content, query) {
results = append(results, f)
}
}
return results, nil
}
func (m *procMockFactStore) GetExpired(_ context.Context) ([]*memory.Fact, error) { return nil, nil }
func (m *procMockFactStore) RefreshTTL(_ context.Context, _ string) error { return nil }
func (m *procMockFactStore) TouchFact(_ context.Context, _ string) error { return nil }
func (m *procMockFactStore) GetColdFacts(_ context.Context, _ int) ([]*memory.Fact, error) {
return nil, nil
}
func (m *procMockFactStore) CompressFacts(_ context.Context, _ []string, _ string) (string, error) {
return "", nil
}
func (m *procMockFactStore) Stats(_ context.Context) (*memory.FactStoreStats, error) {
return &memory.FactStoreStats{}, nil
}
func (m *procMockFactStore) ListGenes(_ context.Context) ([]*memory.Fact, error) { return nil, nil }
func contains(s, sub string) bool {
return len(s) >= len(sub) && (s == sub || len(sub) == 0 ||
(len(s) > 0 && len(sub) > 0 && containsStr(s, sub)))
}
func containsStr(s, sub string) bool {
for i := 0; i <= len(s)-len(sub); i++ {
if s[i:i+len(sub)] == sub {
return true
}
}
return false
}
// --- Tests ---
func TestInteractionProcessor_ProcessStartup_NoEntries(t *testing.T) {
db, err := sqlite.OpenMemory()
require.NoError(t, err)
defer db.Close()
repo, err := sqlite.NewInteractionLogRepo(db)
require.NoError(t, err)
store := &procMockFactStore{}
proc := NewInteractionProcessor(repo, store)
summary, err := proc.ProcessStartup(context.Background())
require.NoError(t, err)
assert.Empty(t, summary)
assert.Empty(t, store.facts, "no facts should be created")
}
func TestInteractionProcessor_ProcessStartup_CreatesFactAndMarksProcessed(t *testing.T) {
db, err := sqlite.OpenMemory()
require.NoError(t, err)
defer db.Close()
repo, err := sqlite.NewInteractionLogRepo(db)
require.NoError(t, err)
ctx := context.Background()
// Insert some tool calls
require.NoError(t, repo.Record(ctx, "add_fact", map[string]interface{}{"content": "test fact about architecture"}))
require.NoError(t, repo.Record(ctx, "search_facts", map[string]interface{}{"query": "security"}))
require.NoError(t, repo.Record(ctx, "health", nil))
require.NoError(t, repo.Record(ctx, "add_fact", map[string]interface{}{"content": "another fact"}))
require.NoError(t, repo.Record(ctx, "dashboard", nil))
store := &procMockFactStore{}
proc := NewInteractionProcessor(repo, store)
summary, err := proc.ProcessStartup(ctx)
require.NoError(t, err)
assert.NotEmpty(t, summary)
assert.Contains(t, summary, "Session summary")
assert.Contains(t, summary, "5 tool calls")
assert.Contains(t, summary, "add_fact(2)")
// Fact should be saved
require.Len(t, store.facts, 1)
assert.Equal(t, memory.LevelDomain, store.facts[0].Level)
assert.Equal(t, "session-history", store.facts[0].Domain)
assert.Equal(t, "auto:interaction-processor", store.facts[0].Source)
// All entries should be marked processed
_, unprocessed, err := repo.Count(ctx)
require.NoError(t, err)
assert.Equal(t, 0, unprocessed)
}
func TestInteractionProcessor_ProcessShutdown(t *testing.T) {
db, err := sqlite.OpenMemory()
require.NoError(t, err)
defer db.Close()
repo, err := sqlite.NewInteractionLogRepo(db)
require.NoError(t, err)
ctx := context.Background()
require.NoError(t, repo.Record(ctx, "version", nil))
require.NoError(t, repo.Record(ctx, "search_facts", map[string]interface{}{"query": "gomcp"}))
store := &procMockFactStore{}
proc := NewInteractionProcessor(repo, store)
summary, err := proc.ProcessShutdown(ctx)
require.NoError(t, err)
assert.Contains(t, summary, "session ending")
assert.Contains(t, summary, "2 tool calls")
require.Len(t, store.facts, 1)
assert.Equal(t, "auto:session-shutdown", store.facts[0].Source)
}
func TestBuildSessionSummary_ToolCounts(t *testing.T) {
now := time.Now()
entries := []sqlite.InteractionEntry{
{ID: 1, ToolName: "add_fact", Timestamp: now},
{ID: 2, ToolName: "add_fact", Timestamp: now},
{ID: 3, ToolName: "add_fact", Timestamp: now},
{ID: 4, ToolName: "search_facts", Timestamp: now},
{ID: 5, ToolName: "health", Timestamp: now},
}
summary := buildSessionSummary(entries, "test")
assert.Contains(t, summary, "5 tool calls")
assert.Contains(t, summary, "add_fact(3)")
assert.Contains(t, summary, "search_facts(1)")
assert.Contains(t, summary, "health(1)")
}
func TestBuildSessionSummary_Duration(t *testing.T) {
now := time.Now()
entries := []sqlite.InteractionEntry{
{ID: 1, ToolName: "a", Timestamp: now.Add(-30 * time.Minute)},
{ID: 2, ToolName: "b", Timestamp: now},
}
summary := buildSessionSummary(entries, "test")
assert.Contains(t, summary, "30m")
}
func TestBuildSessionSummary_Empty(t *testing.T) {
summary := buildSessionSummary(nil, "test")
assert.Empty(t, summary)
}
func TestBuildSessionSummary_Topics(t *testing.T) {
now := time.Now()
entries := []sqlite.InteractionEntry{
{ID: 1, ToolName: "search_facts", ArgsJSON: `{"query":"architecture"}`, Timestamp: now},
{ID: 2, ToolName: "add_fact", ArgsJSON: `{"content":"security review"}`, Timestamp: now},
}
summary := buildSessionSummary(entries, "test")
assert.Contains(t, summary, "Topics:")
assert.Contains(t, summary, "architecture")
}
func TestGetLastSessionSummary_Found(t *testing.T) {
store := &procMockFactStore{}
f := memory.NewFact("Session summary (test): 5 tool calls", memory.LevelDomain, "session-history", "")
f.Source = "auto:session-shutdown"
store.facts = append(store.facts, f)
result := GetLastSessionSummary(context.Background(), store)
assert.Contains(t, result, "Session summary")
}
func TestGetLastSessionSummary_NotFound(t *testing.T) {
store := &procMockFactStore{}
result := GetLastSessionSummary(context.Background(), store)
assert.Empty(t, result)
}
func TestGetLastSessionSummary_SkipsStale(t *testing.T) {
store := &procMockFactStore{}
f := memory.NewFact("Session summary (test): old", memory.LevelDomain, "session-history", "")
f.IsStale = true
store.facts = append(store.facts, f)
result := GetLastSessionSummary(context.Background(), store)
assert.Empty(t, result)
}
func TestFormatDuration(t *testing.T) {
assert.Equal(t, "30s", formatDuration(30*time.Second))
assert.Equal(t, "5m", formatDuration(5*time.Minute))
assert.Equal(t, "2h15m", formatDuration(2*time.Hour+15*time.Minute))
}
func TestExtractTopicsFromEntries(t *testing.T) {
entries := []sqlite.InteractionEntry{
{ArgsJSON: `{"query":"architecture"}`},
{ArgsJSON: `{"content":"security review"}`},
{ArgsJSON: ``}, // empty
}
topics := extractTopicsFromEntries(entries)
assert.NotEmpty(t, topics)
}