Files
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

651 lines
21 KiB
Go

package mcp
import (
"context"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/mark3labs/mcp-go/mcp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/persistence"
)
// ---------------------------------------------------------------------------
// memoryManager — CRUD + filter unit tests
// ---------------------------------------------------------------------------
func TestMemoryManager_SaveQueryDelete(t *testing.T) {
mm := newMemoryManager("", "")
id1, err := mm.Save(persistence.MemoryEntry{
Title: "Bar invariant",
Body: "Bar must hold the lock before mutating cache",
SymbolIDs: []string{"pkg/foo.go::Bar"},
Tags: []string{"invariant"},
Kind: "invariant",
})
require.NoError(t, err)
require.NotEmpty(t, id1)
id2, err := mm.Save(persistence.MemoryEntry{
Body: "TODO: revisit timeout in worker",
FilePaths: []string{"pkg/foo.go"},
Tags: []string{"gotcha"},
Kind: "gotcha",
})
require.NoError(t, err)
all := mm.Query(MemoryQueryFilter{})
require.Len(t, all, 2)
bySymbol := mm.Query(MemoryQueryFilter{SymbolID: "pkg/foo.go::Bar"})
require.Len(t, bySymbol, 1)
assert.Equal(t, id1, bySymbol[0].ID)
byFile := mm.Query(MemoryQueryFilter{FilePath: "pkg/foo.go"})
require.Len(t, byFile, 1)
assert.Equal(t, id2, byFile[0].ID)
byTag := mm.Query(MemoryQueryFilter{Tag: "INVARIANT"})
require.Len(t, byTag, 1)
assert.Equal(t, id1, byTag[0].ID)
byKind := mm.Query(MemoryQueryFilter{Kind: "gotcha"})
require.Len(t, byKind, 1)
assert.Equal(t, id2, byKind[0].ID)
byText := mm.Query(MemoryQueryFilter{TextSearch: "TIMEOUT"})
require.Len(t, byText, 1)
assert.Equal(t, id2, byText[0].ID)
// Title also matched by TextSearch.
byTitle := mm.Query(MemoryQueryFilter{TextSearch: "invariant"})
require.NotEmpty(t, byTitle)
require.NoError(t, mm.Delete(id1))
assert.Equal(t, 1, mm.Count())
require.NoError(t, mm.Delete(id1), "deleting twice is a noop")
}
func TestMemoryManager_Update(t *testing.T) {
mm := newMemoryManager("", "")
id, err := mm.Save(persistence.MemoryEntry{
Body: "draft body",
Tags: []string{"draft"},
})
require.NoError(t, err)
newBody := "final body referring to Bar"
pinned := true
importance := 5
updated, err := mm.Update(id, MemoryPatch{
Body: &newBody,
Tags: []string{"final"},
Pinned: &pinned,
Importance: &importance,
AddLinks: []string{"pkg/x.go::Bar"},
})
require.NoError(t, err)
assert.Equal(t, newBody, updated.Body)
assert.Equal(t, []string{"final"}, updated.Tags)
assert.True(t, updated.Pinned)
assert.Equal(t, 5, updated.Importance)
assert.Contains(t, updated.AutoLinks, "pkg/x.go::Bar")
got, ok := mm.Get(id)
require.True(t, ok)
assert.Equal(t, updated.Body, got.Body)
}
func TestMemoryManager_UpdateMissingID(t *testing.T) {
mm := newMemoryManager("", "")
_, err := mm.Update("nope", MemoryPatch{})
require.Error(t, err)
assert.Contains(t, err.Error(), "not found")
}
func TestMemoryManager_SortPinnedThenImportance(t *testing.T) {
mm := newMemoryManager("", "")
// importance=2, not pinned, newest
_, _ = mm.Save(persistence.MemoryEntry{Body: "low-imp newest", Importance: 2})
// importance=5, pinned, oldest
_, _ = mm.Save(persistence.MemoryEntry{Body: "pinned high-imp", Importance: 5, Pinned: true})
// importance=4, not pinned, middle
_, _ = mm.Save(persistence.MemoryEntry{Body: "mid-imp", Importance: 4})
out := mm.Query(MemoryQueryFilter{})
require.Len(t, out, 3)
assert.True(t, out[0].Pinned, "pinned first")
assert.Equal(t, 4, out[1].Importance, "then importance=4")
assert.Equal(t, 2, out[2].Importance, "then importance=2")
}
func TestMemoryManager_HidesSuperseded(t *testing.T) {
mm := newMemoryManager("", "")
oldID, _ := mm.Save(persistence.MemoryEntry{Body: "old"})
newID, _ := mm.Save(persistence.MemoryEntry{Body: "new"})
supBy := newID
_, err := mm.Update(oldID, MemoryPatch{SupersededBy: &supBy})
require.NoError(t, err)
out := mm.Query(MemoryQueryFilter{})
require.Len(t, out, 1, "superseded entry hidden by default")
assert.Equal(t, newID, out[0].ID)
out2 := mm.Query(MemoryQueryFilter{IncludeSuperseded: true})
require.Len(t, out2, 2)
}
func TestMemoryManager_MarkAccessed(t *testing.T) {
mm := newMemoryManager("", "")
id, _ := mm.Save(persistence.MemoryEntry{Body: "x"})
mm.MarkAccessed([]string{id})
got, _ := mm.Get(id)
assert.EqualValues(t, 1, got.AccessCount)
assert.False(t, got.LastAccessed.IsZero())
}
func TestMemoryManager_DefaultsApplied(t *testing.T) {
mm := newMemoryManager("", "")
id, _ := mm.Save(persistence.MemoryEntry{Body: "minimal"})
got, _ := mm.Get(id)
assert.Equal(t, float32(1.0), got.Confidence)
assert.Equal(t, 3, got.Importance)
assert.Equal(t, "reference", got.Kind)
assert.Equal(t, "manual", got.Source)
}
func TestMemoryManager_PersistenceRoundTrip(t *testing.T) {
tmp := t.TempDir()
repoPath := filepath.Join(tmp, "fake-repo")
require.NoError(t, os.MkdirAll(repoPath, 0o755))
cacheDir := filepath.Join(tmp, "cache")
mm := newMemoryManager(cacheDir, repoPath)
_, err := mm.Save(persistence.MemoryEntry{
Body: "remember this",
SymbolIDs: []string{"pkg/x.go::Foo"},
Tags: []string{"invariant"},
Pinned: true,
})
require.NoError(t, err)
mm2 := newMemoryManager(cacheDir, repoPath)
out := mm2.Query(MemoryQueryFilter{})
require.Len(t, out, 1)
assert.Equal(t, "remember this", out[0].Body)
assert.True(t, out[0].Pinned)
}
// ---------------------------------------------------------------------------
// Surface — ranking behaviour
// ---------------------------------------------------------------------------
func TestSurface_RanksSymbolAnchorAbovePinned(t *testing.T) {
mm := newMemoryManager("", "")
// Pinned but no anchor match.
_, _ = mm.Save(persistence.MemoryEntry{
Body: "pinned unrelated", Pinned: true, Importance: 3,
})
// Symbol anchor match — should rank above pinned.
anchorID, _ := mm.Save(persistence.MemoryEntry{
Body: "anchor match",
SymbolIDs: []string{"pkg/foo.go::Bar"},
Importance: 3,
})
res := mm.Surface(SurfaceOptions{
SymbolIDs: []string{"pkg/foo.go::Bar"},
Limit: 10,
}, nil)
require.Equal(t, 2, res.Total)
assert.Equal(t, anchorID, res.Memories[0].ID, "anchor match should rank above pinned-without-anchor")
}
func TestSurface_KeywordHits(t *testing.T) {
mm := newMemoryManager("", "")
id, _ := mm.Save(persistence.MemoryEntry{Body: "the JWT validator rejects expired tokens"})
_, _ = mm.Save(persistence.MemoryEntry{Body: "unrelated note about caching"})
res := mm.Surface(SurfaceOptions{
Task: "fix jwt validator regression",
Limit: 10,
}, nil)
require.NotEmpty(t, res.Memories)
assert.Equal(t, id, res.Memories[0].ID)
}
func TestSurface_IgnoresSupersededByDefault(t *testing.T) {
mm := newMemoryManager("", "")
oldID, _ := mm.Save(persistence.MemoryEntry{
Body: "old", SymbolIDs: []string{"pkg/x.go::Y"},
})
newID, _ := mm.Save(persistence.MemoryEntry{
Body: "new", SymbolIDs: []string{"pkg/x.go::Y"},
})
supBy := newID
_, _ = mm.Update(oldID, MemoryPatch{SupersededBy: &supBy})
res := mm.Surface(SurfaceOptions{
SymbolIDs: []string{"pkg/x.go::Y"}, Limit: 10,
}, nil)
require.Equal(t, 1, res.Total)
assert.Equal(t, newID, res.Memories[0].ID)
res2 := mm.Surface(SurfaceOptions{
SymbolIDs: []string{"pkg/x.go::Y"}, Limit: 10, IncludeSuperseded: true,
}, nil)
require.Equal(t, 2, res2.Total)
}
func TestSurface_FilePromotionFromSymbol(t *testing.T) {
g := graph.New()
g.AddNode(&graph.Node{ID: "pkg/foo.go::Bar", Name: "Bar", FilePath: "pkg/foo.go", Kind: graph.KindFunction})
mm := newMemoryManager("", "")
id, _ := mm.Save(persistence.MemoryEntry{
Body: "file-anchored memory",
FilePaths: []string{"pkg/foo.go"},
})
res := mm.Surface(SurfaceOptions{
SymbolIDs: []string{"pkg/foo.go::Bar"}, Limit: 10,
}, func(symID string) *graph.Node { return g.GetNode(symID) })
require.Equal(t, 1, res.Total, "containing file of the anchor symbol should match file-anchored memory")
assert.Equal(t, id, res.Memories[0].ID)
}
func TestSurface_RequiresAnchorMatchWhenAnchorsProvided(t *testing.T) {
mm := newMemoryManager("", "")
// Not pinned, no anchor match.
_, _ = mm.Save(persistence.MemoryEntry{Body: "unrelated"})
res := mm.Surface(SurfaceOptions{
SymbolIDs: []string{"pkg/foo.go::Bar"}, Limit: 10,
}, nil)
assert.Equal(t, 0, res.Total, "no-match non-pinned memory must be dropped when anchors provided")
}
func TestSurface_NoAnchorsReturnsEverything(t *testing.T) {
mm := newMemoryManager("", "")
_, _ = mm.Save(persistence.MemoryEntry{Body: "a"})
_, _ = mm.Save(persistence.MemoryEntry{Body: "b"})
res := mm.Surface(SurfaceOptions{Limit: 10}, nil)
assert.Equal(t, 2, res.Total)
}
func TestSurface_ExcerptCap(t *testing.T) {
mm := newMemoryManager("", "")
body := strings.Repeat("A", 1000)
_, _ = mm.Save(persistence.MemoryEntry{Body: body})
res := mm.Surface(SurfaceOptions{Limit: 10, ExcerptCap: 100}, nil)
require.NotEmpty(t, res.Memories)
assert.LessOrEqual(t, len([]byte(res.Memories[0].Body)), 100+len("…")+3)
assert.Contains(t, res.Memories[0].Body, "…")
}
func TestSurface_MarkAccessedIncrements(t *testing.T) {
mm := newMemoryManager("", "")
id, _ := mm.Save(persistence.MemoryEntry{Body: "x"})
_ = mm.Surface(SurfaceOptions{Limit: 10, MarkAccessed: true}, nil)
got, _ := mm.Get(id)
assert.EqualValues(t, 1, got.AccessCount)
}
// ---------------------------------------------------------------------------
// Handler tests — end-to-end against an embedded Server
// ---------------------------------------------------------------------------
func newMemoryTestServer(t *testing.T) *Server {
t.Helper()
g := graph.New()
g.AddNode(&graph.Node{ID: "pkg/foo.go::Bar", Name: "Bar", Kind: graph.KindFunction, FilePath: "pkg/foo.go"})
g.AddNode(&graph.Node{ID: "pkg/foo.go::Baz", Name: "Baz", Kind: graph.KindMethod, FilePath: "pkg/foo.go"})
s := &Server{
graph: g,
session: newSessionState(),
tokenStats: &tokenStats{},
symHistory: &symbolHistory{entries: make(map[string][]SymbolModification)},
sessions: newSessionMap(),
toolScopes: newScopeRegistry(),
}
s.memories = newMemoryManager("", "")
return s
}
func callMemHandler(t *testing.T, h func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error), args map[string]any) *mcp.CallToolResult {
t.Helper()
req := mcp.CallToolRequest{}
req.Params.Arguments = args
res, err := h(context.Background(), req)
require.NoError(t, err)
require.NotNil(t, res)
return res
}
func unmarshalMemResult(t *testing.T, res *mcp.CallToolResult) map[string]any {
t.Helper()
require.False(t, res.IsError, "handler returned an error result: %+v", res.Content)
require.NotEmpty(t, res.Content)
tc, ok := res.Content[0].(mcp.TextContent)
require.True(t, ok, "expected TextContent, got %T", res.Content[0])
var m map[string]any
require.NoError(t, json.Unmarshal([]byte(tc.Text), &m))
return m
}
func TestHandleStoreMemory_CreateAndAutoLink(t *testing.T) {
s := newMemoryTestServer(t)
res := callMemHandler(t, s.handleStoreMemory, map[string]any{
"title": "Bar invariant",
"body": "Bar and Baz must run under the same lock",
"symbol_ids": "pkg/foo.go::Bar",
"tags": "invariant,decision",
"kind": "invariant",
"importance": 5,
})
out := unmarshalMemResult(t, res)
require.NotEmpty(t, out["id"])
assert.Equal(t, "Bar invariant", out["title"])
assert.Equal(t, "invariant", out["kind"])
assert.EqualValues(t, 5, out["importance"].(float64))
symbols, _ := out["symbol_ids"].([]any)
require.NotEmpty(t, symbols)
links, _ := out["links"].([]any)
idSet := map[string]bool{}
for _, l := range links {
idSet[l.(string)] = true
}
assert.True(t, idSet["pkg/foo.go::Baz"], "auto-linker should pick up Baz")
}
func TestHandleStoreMemory_RejectEmpty(t *testing.T) {
s := newMemoryTestServer(t)
req := mcp.CallToolRequest{}
req.Params.Arguments = map[string]any{}
res, err := s.handleStoreMemory(context.Background(), req)
require.NoError(t, err)
require.NotNil(t, res)
assert.True(t, res.IsError, "empty store_memory should return error result")
}
func TestHandleStoreMemory_Update(t *testing.T) {
s := newMemoryTestServer(t)
res := callMemHandler(t, s.handleStoreMemory, map[string]any{
"body": "draft",
})
out := unmarshalMemResult(t, res)
id := out["id"].(string)
res2 := callMemHandler(t, s.handleStoreMemory, map[string]any{
"id": id,
"body": "final body referring to Bar",
"tags": "final",
"pinned": true,
"importance": 5,
})
out2 := unmarshalMemResult(t, res2)
assert.Equal(t, id, out2["id"])
assert.Equal(t, "final body referring to Bar", out2["body"])
assert.Equal(t, true, out2["pinned"])
}
func TestHandleStoreMemory_SupersedesOlder(t *testing.T) {
s := newMemoryTestServer(t)
oldRes := callMemHandler(t, s.handleStoreMemory, map[string]any{
"body": "old wisdom",
"symbol_ids": "pkg/foo.go::Bar",
})
oldID := unmarshalMemResult(t, oldRes)["id"].(string)
newRes := callMemHandler(t, s.handleStoreMemory, map[string]any{
"body": "new wisdom",
"symbol_ids": "pkg/foo.go::Bar",
"supersedes": oldID,
})
newID := unmarshalMemResult(t, newRes)["id"].(string)
old, ok := s.memories.Get(oldID)
require.True(t, ok)
assert.Equal(t, newID, old.SupersededBy)
// query_memories must hide the old one by default.
qres := callMemHandler(t, s.handleQueryMemories, map[string]any{})
q := unmarshalMemResult(t, qres)
total := int(q["total"].(float64))
assert.Equal(t, 1, total)
}
func TestHandleQueryMemories_FilterByKindAndTag(t *testing.T) {
s := newMemoryTestServer(t)
_ = callMemHandler(t, s.handleStoreMemory, map[string]any{
"body": "alpha", "symbol_ids": "pkg/foo.go::Bar", "tags": "invariant", "kind": "invariant",
})
_ = callMemHandler(t, s.handleStoreMemory, map[string]any{
"body": "beta", "symbol_ids": "pkg/foo.go::Baz", "tags": "gotcha", "kind": "gotcha",
})
res := callMemHandler(t, s.handleQueryMemories, map[string]any{"kind": "invariant"})
out := unmarshalMemResult(t, res)
assert.Equal(t, 1, int(out["total"].(float64)))
res2 := callMemHandler(t, s.handleQueryMemories, map[string]any{"tag": "gotcha"})
out2 := unmarshalMemResult(t, res2)
assert.Equal(t, 1, int(out2["total"].(float64)))
}
func TestHandleQueryMemories_TextAndSince(t *testing.T) {
s := newMemoryTestServer(t)
_ = callMemHandler(t, s.handleStoreMemory, map[string]any{"body": "TIMEOUT bug in worker"})
_ = callMemHandler(t, s.handleStoreMemory, map[string]any{"body": "unrelated memory"})
res := callMemHandler(t, s.handleQueryMemories, map[string]any{"text": "timeout"})
out := unmarshalMemResult(t, res)
assert.Equal(t, 1, int(out["total"].(float64)))
future := time.Now().Add(time.Hour).UTC().Format(time.RFC3339)
resFuture := callMemHandler(t, s.handleQueryMemories, map[string]any{"since": future})
outFuture := unmarshalMemResult(t, resFuture)
assert.Equal(t, 0, int(outFuture["total"].(float64)))
}
func TestHandleQueryMemories_BadSince(t *testing.T) {
s := newMemoryTestServer(t)
req := mcp.CallToolRequest{}
req.Params.Arguments = map[string]any{"since": "not-a-timestamp"}
res, err := s.handleQueryMemories(context.Background(), req)
require.NoError(t, err)
require.NotNil(t, res)
assert.True(t, res.IsError)
}
func TestHandleSurfaceMemories_RanksAnchorMatch(t *testing.T) {
s := newMemoryTestServer(t)
_ = callMemHandler(t, s.handleStoreMemory, map[string]any{
"body": "pinned unrelated", "pinned": true,
})
anchorRes := callMemHandler(t, s.handleStoreMemory, map[string]any{
"body": "anchor",
"symbol_ids": "pkg/foo.go::Bar",
})
anchorID := unmarshalMemResult(t, anchorRes)["id"].(string)
res := callMemHandler(t, s.handleSurfaceMemories, map[string]any{
"symbol_ids": "pkg/foo.go::Bar",
"mark_accessed": false,
})
out := unmarshalMemResult(t, res)
mems, _ := out["memories"].([]any)
require.NotEmpty(t, mems)
first := mems[0].(map[string]any)
assert.Equal(t, anchorID, first["id"])
reasons, _ := first["match_reasons"].([]any)
assert.NotEmpty(t, reasons)
}
func TestHandleSurfaceMemories_TaskKeywordSearch(t *testing.T) {
s := newMemoryTestServer(t)
_ = callMemHandler(t, s.handleStoreMemory, map[string]any{
"body": "the JWT validator handles expired tokens",
})
_ = callMemHandler(t, s.handleStoreMemory, map[string]any{
"body": "completely unrelated note",
})
res := callMemHandler(t, s.handleSurfaceMemories, map[string]any{
"task": "fix the jwt validator",
"mark_accessed": false,
})
out := unmarshalMemResult(t, res)
mems, _ := out["memories"].([]any)
require.NotEmpty(t, mems)
body := mems[0].(map[string]any)["body"].(string)
assert.Contains(t, body, "JWT")
}
func TestHandleSurfaceMemories_MarkAccessedDefaultsTrue(t *testing.T) {
s := newMemoryTestServer(t)
storeRes := callMemHandler(t, s.handleStoreMemory, map[string]any{
"body": "anchored", "symbol_ids": "pkg/foo.go::Bar",
})
id := unmarshalMemResult(t, storeRes)["id"].(string)
_ = callMemHandler(t, s.handleSurfaceMemories, map[string]any{
"symbol_ids": "pkg/foo.go::Bar",
// mark_accessed not set — default true.
})
got, _ := s.memories.Get(id)
assert.EqualValues(t, 1, got.AccessCount)
}
// Registration smoke test — protects against accidental removal of
// the registerMemoriesTools call from NewServer.
func TestRegisterMemoriesTools_Wired(t *testing.T) {
s := newMemoryTestServer(t)
require.NotNil(t, s.memories)
// The handlers themselves are exercised by the tests above; this
// stub confirms the manager pointer survives construction.
}
// check_onboarding_performed — readiness probe for the gortex-onboarding
// skill. Performed iff every essential kind clears min_per_kind.
func TestCheckOnboardingPerformed_EmptyStore(t *testing.T) {
s := newMemoryTestServer(t)
res := callMemHandler(t, s.handleCheckOnboardingPerformed, map[string]any{})
out := unmarshalMemResult(t, res)
assert.Equal(t, false, out["performed"])
assert.EqualValues(t, 0, out["total_memories"].(float64))
missing, _ := out["missing_kinds"].([]any)
assert.Len(t, missing, len(defaultEssentialKinds), "every default kind is missing in an empty store")
}
func TestCheckOnboardingPerformed_AllKindsSatisfied(t *testing.T) {
s := newMemoryTestServer(t)
for _, kind := range defaultEssentialKinds {
_ = callMemHandler(t, s.handleStoreMemory, map[string]any{
"body": kind + " memory",
"kind": kind,
})
}
res := callMemHandler(t, s.handleCheckOnboardingPerformed, map[string]any{})
out := unmarshalMemResult(t, res)
assert.Equal(t, true, out["performed"])
missing, _ := out["missing_kinds"].([]any)
assert.Empty(t, missing, "no kinds should be missing when each has a memory")
counts := out["counts_by_kind"].(map[string]any)
for _, k := range defaultEssentialKinds {
assert.EqualValues(t, 1, counts[k].(float64), "kind %s count", k)
}
}
func TestCheckOnboardingPerformed_OneKindMissing(t *testing.T) {
s := newMemoryTestServer(t)
// Only invariant + decision; convention deliberately absent.
_ = callMemHandler(t, s.handleStoreMemory, map[string]any{"body": "x", "kind": "invariant"})
_ = callMemHandler(t, s.handleStoreMemory, map[string]any{"body": "y", "kind": "decision"})
res := callMemHandler(t, s.handleCheckOnboardingPerformed, map[string]any{})
out := unmarshalMemResult(t, res)
assert.Equal(t, false, out["performed"])
missing, _ := out["missing_kinds"].([]any)
require.Len(t, missing, 1)
assert.Equal(t, "convention", missing[0].(string))
}
func TestCheckOnboardingPerformed_CustomKindsAndThresholds(t *testing.T) {
s := newMemoryTestServer(t)
// Two gotchas, one reference.
_ = callMemHandler(t, s.handleStoreMemory, map[string]any{"body": "g1", "kind": "gotcha"})
_ = callMemHandler(t, s.handleStoreMemory, map[string]any{"body": "g2", "kind": "gotcha"})
_ = callMemHandler(t, s.handleStoreMemory, map[string]any{"body": "r1", "kind": "reference"})
res := callMemHandler(t, s.handleCheckOnboardingPerformed, map[string]any{
"essential_kinds": "gotcha,reference",
"min_per_kind": 2,
})
out := unmarshalMemResult(t, res)
// Gotcha clears 2; reference is at 1 (< min 2).
assert.Equal(t, false, out["performed"])
missing, _ := out["missing_kinds"].([]any)
require.Len(t, missing, 1)
assert.Equal(t, "reference", missing[0].(string))
}
func TestCheckOnboardingPerformed_MinTotalGate(t *testing.T) {
s := newMemoryTestServer(t)
// Satisfy all essential kinds, but min_total=10 demands more.
for _, k := range defaultEssentialKinds {
_ = callMemHandler(t, s.handleStoreMemory, map[string]any{"body": k, "kind": k})
}
res := callMemHandler(t, s.handleCheckOnboardingPerformed, map[string]any{"min_total": 10})
out := unmarshalMemResult(t, res)
assert.Equal(t, false, out["performed"], "min_total=10 not reached with 3 memories")
res2 := callMemHandler(t, s.handleCheckOnboardingPerformed, map[string]any{"min_total": 3})
out2 := unmarshalMemResult(t, res2)
assert.Equal(t, true, out2["performed"], "min_total=3 satisfied by 3 memories")
}
func TestCheckOnboardingPerformed_EmptyEssentialKinds(t *testing.T) {
s := newMemoryTestServer(t)
_ = callMemHandler(t, s.handleStoreMemory, map[string]any{"body": "anything"})
res := callMemHandler(t, s.handleCheckOnboardingPerformed, map[string]any{
"essential_kinds": "",
})
out := unmarshalMemResult(t, res)
// No essential kinds → no missing entries; performed gated only by min_total (default 0).
assert.Equal(t, true, out["performed"])
missing, _ := out["missing_kinds"].([]any)
assert.Empty(t, missing)
}