mirror of
https://github.com/syntrex-lab/gomcp.git
synced 2026-04-25 12:26:22 +02:00
- 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
251 lines
8 KiB
Go
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)
|
|
}
|