This commit is contained in:
2026-06-22 08:55:57 +08:00
parent dbb87823e8
commit 6ade6e8fa9
325 changed files with 41131 additions and 855 deletions
+219
View File
@@ -0,0 +1,219 @@
package mcp
import (
"context"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/yan1h/agent-coding-workflow/internal/codeindex"
"github.com/yan1h/agent-coding-workflow/internal/projectmemory"
"github.com/yan1h/agent-coding-workflow/internal/workspace"
)
// ===== fakes =====
type fakeCodeIndexSvc struct {
snippets []codeindex.Snippet
lastWS uuid.UUID
lastQuery codeindex.SearchQuery
enqueuedWS uuid.UUID
}
func (f *fakeCodeIndexSvc) EnqueueBuild(_ context.Context, wsID uuid.UUID, _ *uuid.UUID, _ string) (*codeindex.Run, error) {
f.enqueuedWS = wsID
return &codeindex.Run{ID: uuid.New(), WorkspaceID: wsID}, nil
}
func (f *fakeCodeIndexSvc) Search(_ context.Context, wsID uuid.UUID, q codeindex.SearchQuery) ([]codeindex.Snippet, error) {
f.lastWS = wsID
f.lastQuery = q
return f.snippets, nil
}
func (f *fakeCodeIndexSvc) MarkStale(context.Context, uuid.UUID) error { return nil }
type fakeMemorySvc struct {
written *projectmemory.WriteInput
searched *projectmemory.Query
entries []projectmemory.Entry
}
func (f *fakeMemorySvc) Write(_ context.Context, in projectmemory.WriteInput) (*projectmemory.Entry, error) {
f.written = &in
return &projectmemory.Entry{ID: uuid.New(), ProjectID: in.ProjectID, Kind: in.Kind, Title: in.Title}, nil
}
func (f *fakeMemorySvc) Search(_ context.Context, q projectmemory.Query) ([]projectmemory.Entry, error) {
f.searched = &q
return f.entries, nil
}
func (f *fakeMemorySvc) List(_ context.Context, _ uuid.UUID, _ string) ([]projectmemory.Entry, error) {
return f.entries, nil
}
func (f *fakeMemorySvc) Delete(context.Context, uuid.UUID) error { return nil }
func (f *fakeMemorySvc) RenderAgentsMD(context.Context, uuid.UUID, *uuid.UUID) (string, error) {
return "", nil
}
// depsWithRAG builds testDeps plus a workspace + the code index/memory services.
func depsWithRAG(ci codeindex.Service, mem projectmemory.Service) ServerDeps {
deps := testDeps()
ws := &workspace.Workspace{
ID: testWSID, ProjectID: testPID, Slug: "ws-1", Name: "WS",
DefaultBranch: "main", SyncStatus: workspace.SyncStatusIdle,
}
deps.Workspaces = &fakeWorkspaceSvc{items: []*workspace.Workspace{ws}}
deps.CodeIndex = ci
deps.Memory = mem
return deps
}
// ===== search_code =====
func TestSearchCode_HappyPath(t *testing.T) {
ci := &fakeCodeIndexSvc{snippets: []codeindex.Snippet{
{FilePath: "a.go", StartLine: 1, EndLine: 10, Content: "func A", Score: 0.9, CommitSHA: "abc"},
}}
deps := depsWithRAG(ci, &fakeMemorySvc{})
result, err := callTool(ctxWithAuth(Scope{ProjectIDs: []uuid.UUID{testPID}}), deps,
"search_code", map[string]any{"workspace_id": testWSID.String(), "query": "find A", "top_k": 5})
require.NoError(t, err)
require.False(t, result.IsError)
var out searchCodeOut
unmarshalStructured(t, result, &out)
require.Len(t, out.Results, 1)
assert.Equal(t, "a.go", out.Results[0].File)
assert.Equal(t, "find A", ci.lastQuery.Text)
assert.Equal(t, 5, ci.lastQuery.TopK)
assert.Equal(t, testWSID, ci.lastWS)
}
func TestSearchCode_CrossTenantRejected(t *testing.T) {
deps := depsWithRAG(&fakeCodeIndexSvc{}, &fakeMemorySvc{})
// Scope allows only a different project → project scope check must reject.
result, err := callTool(ctxWithAuth(Scope{ProjectIDs: []uuid.UUID{uuid.New()}}), deps,
"search_code", map[string]any{"workspace_id": testWSID.String(), "query": "q"})
if err == nil {
require.True(t, result.IsError, "cross-tenant workspace must be rejected")
}
}
func TestSearchCode_ToolScopeDenied(t *testing.T) {
deps := depsWithRAG(&fakeCodeIndexSvc{}, &fakeMemorySvc{})
// Token scope only allows memory_list → search_code denied.
result, err := callTool(ctxWithAuth(Scope{Tools: []string{"memory_list"}}), deps,
"search_code", map[string]any{"workspace_id": testWSID.String(), "query": "q"})
if err == nil {
require.True(t, result.IsError)
}
}
func TestReindexWorkspace_HappyPath(t *testing.T) {
ci := &fakeCodeIndexSvc{}
deps := depsWithRAG(ci, &fakeMemorySvc{})
result, err := callTool(ctxWithAuth(Scope{ProjectIDs: []uuid.UUID{testPID}}), deps,
"reindex_workspace", map[string]any{"workspace_id": testWSID.String()})
require.NoError(t, err)
require.False(t, result.IsError)
var out reindexWorkspaceOut
unmarshalStructured(t, result, &out)
assert.True(t, out.OK)
assert.NotEmpty(t, out.RunID)
assert.Equal(t, testWSID, ci.enqueuedWS)
}
// When CodeIndex is nil the tool is not registered → call errors (unknown tool).
func TestSearchCode_NotRegisteredWhenDisabled(t *testing.T) {
deps := testDeps()
deps.Memory = &fakeMemorySvc{} // memory present, code index nil
result, err := callTool(ctxWithAuth(Scope{}), deps,
"search_code", map[string]any{"workspace_id": testWSID.String(), "query": "q"})
if err == nil {
require.True(t, result.IsError, "disabled tool must not succeed")
}
}
// ===== memory tools =====
func TestMemoryWrite_HappyPath(t *testing.T) {
mem := &fakeMemorySvc{}
deps := depsWithRAG(&fakeCodeIndexSvc{}, mem)
result, err := callTool(ctxWithAuth(Scope{ProjectIDs: []uuid.UUID{testPID}}), deps,
"memory_write", map[string]any{
"project_slug": "test-project", "kind": "decision",
"title": "Use X", "body": "because Y", "tags": []string{"a", "b"},
})
require.NoError(t, err)
require.False(t, result.IsError)
var out memoryWriteOut
unmarshalStructured(t, result, &out)
assert.True(t, out.OK)
require.NotNil(t, mem.written)
assert.Equal(t, testPID, mem.written.ProjectID)
assert.Equal(t, "decision", mem.written.Kind)
assert.Equal(t, []string{"a", "b"}, mem.written.Tags)
}
func TestMemoryWrite_ByWorkspace(t *testing.T) {
mem := &fakeMemorySvc{}
deps := depsWithRAG(&fakeCodeIndexSvc{}, mem)
result, err := callTool(ctxWithAuth(Scope{ProjectIDs: []uuid.UUID{testPID}}), deps,
"memory_write", map[string]any{
"workspace_id": testWSID.String(), "kind": "gotcha",
"title": "T", "body": "B",
})
require.NoError(t, err)
require.False(t, result.IsError)
require.NotNil(t, mem.written)
assert.Equal(t, testPID, mem.written.ProjectID)
require.NotNil(t, mem.written.WorkspaceID)
assert.Equal(t, testWSID, *mem.written.WorkspaceID)
}
func TestMemorySearch_HappyPath(t *testing.T) {
mem := &fakeMemorySvc{entries: []projectmemory.Entry{
{ID: uuid.New(), Kind: "decision", Title: "hit", Body: "x", Tags: []string{}},
}}
deps := depsWithRAG(&fakeCodeIndexSvc{}, mem)
result, err := callTool(ctxWithAuth(Scope{ProjectIDs: []uuid.UUID{testPID}}), deps,
"memory_search", map[string]any{"project_slug": "test-project", "query": "hit"})
require.NoError(t, err)
require.False(t, result.IsError)
var out memorySearchOut
unmarshalStructured(t, result, &out)
require.Len(t, out.Results, 1)
assert.Equal(t, "hit", out.Results[0].Title)
require.NotNil(t, mem.searched)
assert.Equal(t, testPID, mem.searched.ProjectID)
}
func TestMemoryList_HappyPath(t *testing.T) {
mem := &fakeMemorySvc{entries: []projectmemory.Entry{
{ID: uuid.New(), Kind: "convention", Title: "c", Tags: []string{}},
}}
deps := depsWithRAG(&fakeCodeIndexSvc{}, mem)
result, err := callTool(ctxWithAuth(Scope{ProjectIDs: []uuid.UUID{testPID}}), deps,
"memory_list", map[string]any{"project_slug": "test-project"})
require.NoError(t, err)
require.False(t, result.IsError)
var out memoryListOut
unmarshalStructured(t, result, &out)
require.Len(t, out.Entries, 1)
}
func TestMemoryWrite_CrossTenantRejected(t *testing.T) {
deps := depsWithRAG(&fakeCodeIndexSvc{}, &fakeMemorySvc{})
result, err := callTool(ctxWithAuth(Scope{ProjectIDs: []uuid.UUID{uuid.New()}}), deps,
"memory_write", map[string]any{
"project_slug": "test-project", "kind": "decision", "title": "T", "body": "B",
})
if err == nil {
require.True(t, result.IsError, "cross-tenant project must be rejected")
}
}
var (
_ codeindex.Service = (*fakeCodeIndexSvc)(nil)
_ projectmemory.Service = (*fakeMemorySvc)(nil)
)