Files
zzet--gortex/internal/mcp/tools_memory_hierarchy_test.go
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

273 lines
8.4 KiB
Go

package mcp
import (
"context"
"testing"
"github.com/mark3labs/mcp-go/mcp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func newHierarchyTestServer(t *testing.T) *Server {
t.Helper()
s := newMemoryTestServer(t)
// Mount an in-memory global store. newMemoryManager with empty
// args returns a no-disk manager — sufficient for tests.
s.globalMemories = newMemoryManager("", "")
return s
}
// edit_memory ---------------------------------------------------------
func TestEditMemory_RegexReplace(t *testing.T) {
s := newHierarchyTestServer(t)
res := callMemHandler(t, s.handleStoreMemory, map[string]any{
"body": "uses Mutex everywhere",
})
id := unmarshalMemResult(t, res)["id"].(string)
out := callMemHandler(t, s.handleEditMemory, map[string]any{
"id": id,
"pattern": `Mutex`,
"replacement": "RWMutex",
})
body := unmarshalMemResult(t, out)["body"].(string)
assert.Equal(t, "uses RWMutex everywhere", body)
}
func TestEditMemory_LiteralMode(t *testing.T) {
s := newHierarchyTestServer(t)
res := callMemHandler(t, s.handleStoreMemory, map[string]any{
"body": "panics when .* is nil",
})
id := unmarshalMemResult(t, res)["id"].(string)
out := callMemHandler(t, s.handleEditMemory, map[string]any{
"id": id,
"pattern": `.*`,
"replacement": "the input",
"mode": "literal",
})
body := unmarshalMemResult(t, out)["body"].(string)
assert.Equal(t, "panics when the input is nil", body)
}
func TestEditMemory_RegexGroups(t *testing.T) {
s := newHierarchyTestServer(t)
res := callMemHandler(t, s.handleStoreMemory, map[string]any{
"body": "limit=100",
})
id := unmarshalMemResult(t, res)["id"].(string)
out := callMemHandler(t, s.handleEditMemory, map[string]any{
"id": id,
"pattern": `limit=(\d+)`,
"replacement": "limit=$1 (was here)",
})
body := unmarshalMemResult(t, out)["body"].(string)
assert.Equal(t, "limit=100 (was here)", body)
}
func TestEditMemory_NoChangeIsIdempotent(t *testing.T) {
s := newHierarchyTestServer(t)
res := callMemHandler(t, s.handleStoreMemory, map[string]any{
"body": "untouched",
})
id := unmarshalMemResult(t, res)["id"].(string)
out := callMemHandler(t, s.handleEditMemory, map[string]any{
"id": id,
"pattern": `nonexistent`,
"replacement": "x",
})
body := unmarshalMemResult(t, out)["body"].(string)
assert.Equal(t, "untouched", body)
}
func TestEditMemory_BadRegex(t *testing.T) {
s := newHierarchyTestServer(t)
res := callMemHandler(t, s.handleStoreMemory, map[string]any{"body": "x"})
id := unmarshalMemResult(t, res)["id"].(string)
req := mcp.CallToolRequest{}
req.Params.Arguments = map[string]any{
"id": id,
"pattern": `(`, // unclosed group
}
resp, err := s.handleEditMemory(context.Background(), req)
require.NoError(t, err)
assert.True(t, resp.IsError, "invalid regex should return error result")
}
func TestEditMemory_UnknownID(t *testing.T) {
s := newHierarchyTestServer(t)
req := mcp.CallToolRequest{}
req.Params.Arguments = map[string]any{"id": "does-not-exist", "pattern": "x"}
resp, err := s.handleEditMemory(context.Background(), req)
require.NoError(t, err)
assert.True(t, resp.IsError)
}
func TestEditMemory_RejectsBadMode(t *testing.T) {
s := newHierarchyTestServer(t)
res := callMemHandler(t, s.handleStoreMemory, map[string]any{"body": "x"})
id := unmarshalMemResult(t, res)["id"].(string)
req := mcp.CallToolRequest{}
req.Params.Arguments = map[string]any{"id": id, "pattern": "x", "mode": "nonsense"}
resp, err := s.handleEditMemory(context.Background(), req)
require.NoError(t, err)
assert.True(t, resp.IsError)
}
// global scope -------------------------------------------------------
func TestStoreMemory_GlobalScope(t *testing.T) {
s := newHierarchyTestServer(t)
out := callMemHandler(t, s.handleStoreMemory, map[string]any{
"body": "always use tokio::spawn_blocking for blocking calls",
"kind": "invariant",
"scope": "global",
})
parsed := unmarshalMemResult(t, out)
require.NotEmpty(t, parsed["id"])
// Should be visible in global store.
assert.Equal(t, 1, s.globalMemories.Count())
// Should NOT be in the workspace store.
assert.Equal(t, 0, s.memories.Count())
}
func TestQueryMemories_ScopeFilter(t *testing.T) {
s := newHierarchyTestServer(t)
_ = callMemHandler(t, s.handleStoreMemory, map[string]any{
"body": "in workspace",
"kind": "invariant",
})
_ = callMemHandler(t, s.handleStoreMemory, map[string]any{
"body": "in global",
"kind": "invariant",
"scope": "global",
})
wOnly := unmarshalMemResult(t, callMemHandler(t, s.handleQueryMemories, map[string]any{
"kind": "invariant",
"scope": "workspace",
}))
gOnly := unmarshalMemResult(t, callMemHandler(t, s.handleQueryMemories, map[string]any{
"kind": "invariant",
"scope": "global",
}))
both := unmarshalMemResult(t, callMemHandler(t, s.handleQueryMemories, map[string]any{
"kind": "invariant",
"scope": "both",
}))
assert.EqualValues(t, 1, wOnly["total"].(float64))
assert.EqualValues(t, 1, gOnly["total"].(float64))
assert.EqualValues(t, 2, both["total"].(float64))
// Each row in scope=both should carry an explicit `scope` tag.
mems, _ := both["memories"].([]any)
scopes := map[string]int{}
for _, m := range mems {
scopes[m.(map[string]any)["scope"].(string)]++
}
assert.Equal(t, 1, scopes["workspace"])
assert.Equal(t, 1, scopes["global"])
}
// rename_memory ------------------------------------------------------
func TestRenameMemory_WorkspaceToGlobal(t *testing.T) {
s := newHierarchyTestServer(t)
res := callMemHandler(t, s.handleStoreMemory, map[string]any{
"body": "promote me",
})
id := unmarshalMemResult(t, res)["id"].(string)
out := callMemHandler(t, s.handleRenameMemory, map[string]any{
"id": id,
"to_scope": "global",
})
moved := unmarshalMemResult(t, out)
assert.Equal(t, "global", moved["scope"])
assert.Equal(t, id, moved["previous_id"], "rename surfaces the previous id")
// Source store no longer has it; destination does.
assert.Equal(t, 0, s.memories.Count())
assert.Equal(t, 1, s.globalMemories.Count())
}
func TestRenameMemory_GlobalToWorkspace(t *testing.T) {
s := newHierarchyTestServer(t)
res := callMemHandler(t, s.handleStoreMemory, map[string]any{
"body": "demote me",
"scope": "global",
})
id := unmarshalMemResult(t, res)["id"].(string)
out := callMemHandler(t, s.handleRenameMemory, map[string]any{
"id": id,
"from_scope": "global",
"to_scope": "workspace",
})
moved := unmarshalMemResult(t, out)
assert.Equal(t, "workspace", moved["scope"])
assert.Equal(t, 0, s.globalMemories.Count())
assert.Equal(t, 1, s.memories.Count())
}
func TestRenameMemory_RejectsSameScope(t *testing.T) {
s := newHierarchyTestServer(t)
res := callMemHandler(t, s.handleStoreMemory, map[string]any{"body": "x"})
id := unmarshalMemResult(t, res)["id"].(string)
req := mcp.CallToolRequest{}
req.Params.Arguments = map[string]any{"id": id, "to_scope": "workspace"}
resp, err := s.handleRenameMemory(context.Background(), req)
require.NoError(t, err)
assert.True(t, resp.IsError)
}
func TestRenameMemory_UnknownScope(t *testing.T) {
s := newHierarchyTestServer(t)
req := mcp.CallToolRequest{}
req.Params.Arguments = map[string]any{"id": "anything", "to_scope": "moon"}
resp, err := s.handleRenameMemory(context.Background(), req)
require.NoError(t, err)
assert.True(t, resp.IsError)
}
func TestRenameMemory_UnknownID(t *testing.T) {
s := newHierarchyTestServer(t)
req := mcp.CallToolRequest{}
req.Params.Arguments = map[string]any{"id": "does-not-exist", "to_scope": "global"}
resp, err := s.handleRenameMemory(context.Background(), req)
require.NoError(t, err)
assert.True(t, resp.IsError)
}
func TestResolveMemoryStore(t *testing.T) {
s := newHierarchyTestServer(t)
assert.Equal(t, s.memories, s.resolveMemoryStore(""))
assert.Equal(t, s.memories, s.resolveMemoryStore("workspace"))
assert.Equal(t, s.globalMemories, s.resolveMemoryStore("global"))
// Unknown values fall through to workspace.
assert.Equal(t, s.memories, s.resolveMemoryStore("garbage"))
}
func TestResolveMemoryStores(t *testing.T) {
s := newHierarchyTestServer(t)
assert.Len(t, s.resolveMemoryStores(""), 1)
assert.Len(t, s.resolveMemoryStores("workspace"), 1)
assert.Len(t, s.resolveMemoryStores("global"), 1)
assert.Len(t, s.resolveMemoryStores("both"), 2)
}
func TestDefaultIfEmpty(t *testing.T) {
assert.Equal(t, "fallback", defaultIfEmpty("", "fallback"))
assert.Equal(t, "x", defaultIfEmpty("x", "fallback"))
}