first commit
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user