Files
goclaw/internal/tools/memory_search_topics_test.go
ナムandCollective Developer 84fab59226 fix(memory): guide recency recall tool calls (#1412)
* fix(memory): guide recency recall tool calls

* fix(tools): align memory and vault builtin visibility

---------

Co-authored-by: Collective Developer <man@collective.dev>
2026-07-10 01:24:39 +07:00

204 lines
6.2 KiB
Go

package tools
import (
"context"
"encoding/json"
"strings"
"testing"
"time"
"github.com/google/uuid"
"github.com/nextlevelbuilder/goclaw/internal/store"
)
type memorySearchFakeMemoryStore struct {
store.MemoryStore
results []store.MemorySearchResult
calls int
}
func (f *memorySearchFakeMemoryStore) Search(context.Context, string, string, string, store.MemorySearchOptions) ([]store.MemorySearchResult, error) {
f.calls++
return f.results, nil
}
type memorySearchFakeEpisodicStore struct {
store.EpisodicStore
results []store.EpisodicSearchResult
ep *store.EpisodicSummary
query string
opts store.EpisodicSearchOptions
}
func TestMemorySearchDescriptionGuidesRecencyFilters(t *testing.T) {
tool := NewMemorySearchTool()
desc := tool.Description()
for _, want := range []string{
"recency/date questions",
"query=\"*\"",
"createdAfter/createdBefore",
"timeRange",
"do NOT put date words",
"second semantic search",
} {
if !strings.Contains(desc, want) {
t.Fatalf("description missing %q:\n%s", want, desc)
}
}
props, ok := tool.Parameters()["properties"].(map[string]any)
if !ok {
t.Fatal("tool parameters missing properties")
}
for name, wants := range map[string][]string{
"query": {"query=\"*\"", "createdAfter/createdBefore", "do not put date words"},
"timeRange": {"query=\"*\"", "relative recency requests", "createdAfter and createdBefore"},
"createdAfter": {"query=\"*\"", "calendar windows"},
"createdBefore": {"query=\"*\"", "calendar windows"},
} {
param, ok := props[name].(map[string]any)
if !ok {
t.Fatalf("parameter %q missing", name)
}
got, _ := param["description"].(string)
for _, want := range wants {
if !strings.Contains(got, want) {
t.Fatalf("%s description missing %q:\n%s", name, want, got)
}
}
}
}
func (f *memorySearchFakeEpisodicStore) Search(_ context.Context, query string, _ string, _ string, opts store.EpisodicSearchOptions) ([]store.EpisodicSearchResult, error) {
f.query = query
f.opts = opts
return f.results, nil
}
func (f *memorySearchFakeEpisodicStore) Get(context.Context, string) (*store.EpisodicSummary, error) {
return f.ep, nil
}
func (f *memorySearchFakeEpisodicStore) RecordRecall(context.Context, string, float64) error {
return nil
}
func TestMemorySearchIncludesEpisodicKeyTopics(t *testing.T) {
tool := NewMemorySearchTool()
tool.SetMemoryStore(&memorySearchFakeMemoryStore{})
tool.SetEpisodicStore(&memorySearchFakeEpisodicStore{results: []store.EpisodicSearchResult{
{
EpisodicID: "ep-1",
L0Abstract: "Project Alpha workshop notes mention visual hierarchy.",
KeyTopics: []string{"visual-hierarchy", "Project Alpha", "Workshop Notes"},
Score: 0.8,
CreatedAt: time.Now(),
SessionKey: "channel:design",
},
}})
ctx := store.WithUserID(store.WithAgentID(context.Background(), uuid.New()), "user-1")
res := tool.Execute(ctx, map[string]any{"query": "Project Alpha visual hierarchy"})
if res.IsError {
t.Fatalf("unexpected error: %s", res.ForLLM)
}
var output struct {
Results []struct {
Tier string `json:"tier"`
EpisodicID string `json:"episodic_id"`
KeyTopics []string `json:"key_topics"`
} `json:"results"`
}
if err := json.Unmarshal([]byte(res.ForLLM), &output); err != nil {
t.Fatalf("invalid JSON output: %v\n%s", err, res.ForLLM)
}
if len(output.Results) != 1 {
t.Fatalf("results = %d, want 1: %s", len(output.Results), res.ForLLM)
}
got := output.Results[0]
if got.Tier != "episodic" || got.EpisodicID != "ep-1" {
t.Fatalf("episodic result metadata = %+v", got)
}
if strings.Join(got.KeyTopics, ",") != "visual-hierarchy,Project Alpha,Workshop Notes" {
t.Fatalf("key_topics = %#v", got.KeyTopics)
}
}
func TestMemorySearchTimeRangeListsEpisodicOnly(t *testing.T) {
createdAt := time.Date(2026, 7, 9, 17, 21, 29, 0, time.UTC)
expiresAt := createdAt.Add(7 * 24 * time.Hour)
mem := &memorySearchFakeMemoryStore{}
ep := &memorySearchFakeEpisodicStore{results: []store.EpisodicSearchResult{
{
EpisodicID: "ep-1",
L0Abstract: "Project Alpha workshop notes mention visual hierarchy.",
KeyTopics: []string{"visual-hierarchy", "Project Alpha"},
Score: 1,
CreatedAt: createdAt,
ExpiresAt: &expiresAt,
SessionKey: "channel:design",
},
}}
tool := NewMemorySearchTool()
tool.SetMemoryStore(mem)
tool.SetEpisodicStore(ep)
ctx := store.WithUserID(store.WithAgentID(context.Background(), uuid.New()), "user-1")
res := tool.Execute(ctx, map[string]any{"query": "*", "timeRange": "24h"})
if res.IsError {
t.Fatalf("unexpected error: %s", res.ForLLM)
}
if mem.calls != 0 {
t.Fatalf("document memory search calls = %d, want 0 for query='*'", mem.calls)
}
if ep.query != "*" {
t.Fatalf("episodic query = %q, want *", ep.query)
}
if ep.opts.CreatedAfter == nil {
t.Fatal("CreatedAfter was not forwarded")
}
if ep.opts.IncludeExpired {
t.Fatal("IncludeExpired default = true, want false")
}
var output struct {
Results []struct {
CreatedAt string `json:"created_at"`
ExpiresAt *string `json:"expires_at"`
} `json:"results"`
}
if err := json.Unmarshal([]byte(res.ForLLM), &output); err != nil {
t.Fatalf("invalid JSON output: %v\n%s", err, res.ForLLM)
}
if len(output.Results) != 1 {
t.Fatalf("results = %d, want 1: %s", len(output.Results), res.ForLLM)
}
if output.Results[0].CreatedAt == "" {
t.Fatalf("created_at missing: %s", res.ForLLM)
}
if output.Results[0].ExpiresAt == nil || *output.Results[0].ExpiresAt == "" {
t.Fatalf("expires_at missing: %s", res.ForLLM)
}
}
func TestMemoryExpandIncludesEpisodicKeyTopics(t *testing.T) {
tool := NewMemoryExpandTool()
tool.SetEpisodicStore(&memorySearchFakeEpisodicStore{ep: &store.EpisodicSummary{
L0Abstract: "Project Alpha workshop notes.",
SessionKey: "channel:design",
CreatedAt: time.Date(2026, 7, 9, 14, 55, 0, 0, time.UTC),
KeyTopics: []string{"visual-hierarchy", "Project Alpha", "Workshop Notes"},
Summary: "Visual hierarchy is a focus area.",
}})
res := tool.Execute(context.Background(), map[string]any{"id": "ep-1"})
if res.IsError {
t.Fatalf("unexpected error: %s", res.ForLLM)
}
if !strings.Contains(res.ForLLM, "**Topics:** visual-hierarchy, Project Alpha, Workshop Notes") {
t.Fatalf("topics missing from expanded memory: %s", res.ForLLM)
}
}