Files
Cyrene_v2/backend/services/memory/extractor_test.go
T
Aska 47047c903c feat(memory): add LLM-driven memory extraction from conversations
- memory/extractor: async LLM-based memory extraction from chat history
- Parses JSON lines from LLM response as memory entries
- Wire into orchestrator pipeline via PostProcess hook
- Non-blocking: runs in goroutine with 30s timeout
- Automatic dedup via existing keyword overlap logic
2026-08-12 18:43:04 +08:00

100 lines
2.7 KiB
Go

package memory
import (
"context"
"testing"
"git.yeij.top/AskaEth/Cyrene_v2/services/ai-core/model"
)
func TestParseExtractedMemories(t *testing.T) {
tests := []struct {
input string
minCount int
}{
{`{"content":"用户喜欢咖啡","category":"preference","priority":0.8}
{"content":"用户每天跑步","category":"fact","priority":0.7}`, 2},
{`这是提取的记忆:
{"content":"用户名叫张三","category":"fact","priority":0.9}
{"content":"用户在北京工作","category":"fact","priority":0.85}`, 2},
{`no json here`, 0},
{``, 0},
}
for _, tt := range tests {
t.Run("", func(t *testing.T) {
result := parseExtractedMemories(tt.input)
if len(result) < tt.minCount {
t.Errorf("expected >= %d memories, got %d: %+v", tt.minCount, len(result), result)
}
for _, m := range result {
if m.Content == "" {
t.Error("memory content should not be empty")
}
if m.Category == "" {
t.Error("memory category should not be empty")
}
}
})
}
}
func TestExtractorDisabled(t *testing.T) {
e := NewExtractor(nil, nil)
if e.enabled {
t.Error("extractor should be disabled when llm is nil")
}
// Should not panic
e.ExtractFromConversation(nil, "user-1", nil)
}
func TestExtractorParseEdgeCases(t *testing.T) {
// Test JSON with whitespace
result := parseExtractedMemories(` {"content":"test","category":"fact","priority":0.5} `)
if len(result) != 1 {
t.Errorf("expected 1, got %d", len(result))
}
// Test empty JSON object
result = parseExtractedMemories(`{}`)
if len(result) != 0 {
t.Errorf("expected 0 for empty object, got %d", len(result))
}
// Test invalid JSON line mixed with valid
result = parseExtractedMemories(`invalid
{"content":"ok","category":"fact","priority":0.5}
not json either`)
if len(result) != 1 {
t.Errorf("expected 1 valid, got %d", len(result))
}
}
// mockLLM implements LLMClient for testing.
type mockLLM struct {
response string
err error
}
func (m *mockLLM) Chat(_ context.Context, _ []model.LLMMessage) (*model.LLMResponse, error) {
if m.err != nil { return nil, m.err }
return &model.LLMResponse{Content: m.response, Model: "mock"}, nil
}
func TestExtractorWithMockLLM(t *testing.T) {
mock := &mockLLM{
response: `{"content":"用户喜欢喝咖啡","category":"preference","priority":0.8}
{"content":"用户在2024年去过日本","category":"event","priority":0.9}`,
}
// We can't easily test the full extraction without a real DB
// But verify mock LLM works
resp, err := mock.Chat(nil, nil)
if err != nil { t.Fatalf("mock LLM: %v", err) }
memories := parseExtractedMemories(resp.Content)
if len(memories) != 2 {
t.Errorf("expected 2 memories, got %d", len(memories))
}
}