You've already forked agentic-coding-workflow
bugfix
This commit is contained in:
@@ -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)
|
||||
)
|
||||
Reference in New Issue
Block a user