180 lines
5.6 KiB
Go
180 lines
5.6 KiB
Go
package agenttool
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/smallnest/pigo/internal/agentcore"
|
|
"github.com/smallnest/pigo/internal/memory"
|
|
)
|
|
|
|
// newMemoryStoreWithCorpus opens a *memory.Store over a temp DB + temp mimo root
|
|
// and writes a couple of .md files under the layout. It does NOT reconcile — the
|
|
// tool's ReconcileFirst=true is expected to index them lazily on first search.
|
|
func newMemoryStoreWithCorpus(t *testing.T) *memory.Store {
|
|
t.Helper()
|
|
base := t.TempDir()
|
|
root := filepath.Join(base, "mimo")
|
|
if err := os.MkdirAll(root, 0o755); err != nil {
|
|
t.Fatalf("mkdir root: %v", err)
|
|
}
|
|
writeMemFile(t, root, "permission deadlock encountered during checkpoint save then retry succeeded",
|
|
"projects", "proj1", "notes", "rare.md")
|
|
writeMemFile(t, root, "unrelated grocery shopping list",
|
|
"global", "user", "u1.md")
|
|
|
|
dbPath := filepath.Join(base, "sub", "memory.db")
|
|
st, err := memory.Open(dbPath, root, "")
|
|
if err != nil {
|
|
t.Fatalf("memory.Open: %v", err)
|
|
}
|
|
t.Cleanup(func() { st.Close() })
|
|
return st
|
|
}
|
|
|
|
func writeMemFile(t *testing.T, root, body string, segs ...string) string {
|
|
t.Helper()
|
|
full := filepath.Join(append([]string{root}, segs...)...)
|
|
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
|
|
t.Fatalf("mkdir for %q: %v", full, err)
|
|
}
|
|
if err := os.WriteFile(full, []byte(body), 0o644); err != nil {
|
|
t.Fatalf("write %q: %v", full, err)
|
|
}
|
|
return filepath.Clean(full)
|
|
}
|
|
|
|
func runMemorySearch(t *testing.T, tool *MemorySearchTool, args map[string]any) (string, any) {
|
|
t.Helper()
|
|
raw, err := json.Marshal(args)
|
|
if err != nil {
|
|
t.Fatalf("marshal args: %v", err)
|
|
}
|
|
res, err := tool.Execute(context.Background(), "call-1", raw, nil)
|
|
if err != nil {
|
|
t.Fatalf("Execute returned Go error: %v", err)
|
|
}
|
|
return contentText(res.Content), res.Details
|
|
}
|
|
|
|
// contentText concatenates the text of every TextContent block in a result.
|
|
func contentText(content agentcore.ContentList) string {
|
|
var b strings.Builder
|
|
for _, c := range content {
|
|
if tc, ok := c.(agentcore.TextContent); ok {
|
|
b.WriteString(tc.Text)
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func TestMemorySearchToolInterface(t *testing.T) {
|
|
tool := &MemorySearchTool{}
|
|
if tool.Name() != "memory_search" {
|
|
t.Fatalf("Name = %q, want memory_search", tool.Name())
|
|
}
|
|
if tool.Description() == "" {
|
|
t.Fatal("Description must not be empty")
|
|
}
|
|
// Schema must be valid JSON declaring query as required.
|
|
var schema struct {
|
|
Required []string `json:"required"`
|
|
}
|
|
if err := json.Unmarshal(tool.Schema(), &schema); err != nil {
|
|
t.Fatalf("Schema is not valid JSON: %v", err)
|
|
}
|
|
if len(schema.Required) != 1 || schema.Required[0] != "query" {
|
|
t.Fatalf("Schema required = %v, want [query]", schema.Required)
|
|
}
|
|
}
|
|
|
|
func TestMemorySearchFindsSnippet(t *testing.T) {
|
|
tool := &MemorySearchTool{Store: newMemoryStoreWithCorpus(t)}
|
|
|
|
text, details := runMemorySearch(t, tool, map[string]any{"query": "permission deadlock"})
|
|
|
|
if !strings.Contains(text, "rare.md") {
|
|
t.Fatalf("expected result text to reference rare.md, got:\n%s", text)
|
|
}
|
|
if !strings.Contains(strings.ToLower(text), "permission") {
|
|
t.Fatalf("expected snippet to mention 'permission', got:\n%s", text)
|
|
}
|
|
|
|
// Details must carry the structured results (lazy reconcile indexed the file).
|
|
results, ok := details.([]memory.SearchResult)
|
|
if !ok {
|
|
t.Fatalf("Details type = %T, want []memory.SearchResult", details)
|
|
}
|
|
if len(results) == 0 {
|
|
t.Fatal("expected at least one structured result")
|
|
}
|
|
found := false
|
|
for _, r := range results {
|
|
if strings.HasSuffix(r.Path, filepath.Join("notes", "rare.md")) {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("expected rare.md among structured results, got %+v", results)
|
|
}
|
|
}
|
|
|
|
func TestMemorySearchScopeAndTypeFilter(t *testing.T) {
|
|
tool := &MemorySearchTool{Store: newMemoryStoreWithCorpus(t)}
|
|
|
|
// Filter to the global/user doc; the projects/notes doc must be excluded even
|
|
// though it also matches the shared word.
|
|
text, _ := runMemorySearch(t, tool, map[string]any{
|
|
"query": "grocery permission",
|
|
"scope": "global",
|
|
"type": "user",
|
|
})
|
|
if strings.Contains(text, "rare.md") {
|
|
t.Fatalf("scope/type filter should exclude rare.md, got:\n%s", text)
|
|
}
|
|
if !strings.Contains(text, "u1.md") {
|
|
t.Fatalf("expected u1.md to match global/user filter, got:\n%s", text)
|
|
}
|
|
}
|
|
|
|
func TestMemorySearchNoResults(t *testing.T) {
|
|
tool := &MemorySearchTool{Store: newMemoryStoreWithCorpus(t)}
|
|
text, _ := runMemorySearch(t, tool, map[string]any{"query": "zzzznonexistenttoken"})
|
|
if !strings.Contains(text, "no results") {
|
|
t.Fatalf("expected a clear empty message, got:\n%s", text)
|
|
}
|
|
}
|
|
|
|
func TestMemorySearchEmptyQueryNoOp(t *testing.T) {
|
|
tool := &MemorySearchTool{Store: newMemoryStoreWithCorpus(t)}
|
|
text, _ := runMemorySearch(t, tool, map[string]any{"query": " "})
|
|
if !strings.Contains(text, "empty query") {
|
|
t.Fatalf("expected empty-query no-op message, got:\n%s", text)
|
|
}
|
|
}
|
|
|
|
func TestMemorySearchNilStoreNoOp(t *testing.T) {
|
|
tool := &MemorySearchTool{} // Store nil
|
|
text, _ := runMemorySearch(t, tool, map[string]any{"query": "anything"})
|
|
if !strings.Contains(text, "no memory store") {
|
|
t.Fatalf("expected nil-store no-op message, got:\n%s", text)
|
|
}
|
|
}
|
|
|
|
func TestMemorySearchInvalidArgs(t *testing.T) {
|
|
tool := &MemorySearchTool{Store: newMemoryStoreWithCorpus(t)}
|
|
res, err := tool.Execute(context.Background(), "call-1", json.RawMessage(`{"query": 123}`), nil)
|
|
if err != nil {
|
|
t.Fatalf("Execute returned Go error: %v", err)
|
|
}
|
|
var text strings.Builder
|
|
text.WriteString(contentText(res.Content))
|
|
if !strings.Contains(text.String(), "invalid arguments") {
|
|
t.Fatalf("expected invalid-arguments error result, got:\n%s", text.String())
|
|
}
|
|
}
|