package mcp_test import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "github.com/mathiasbq/hyperguild/ingestion/internal/mcp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func toolCall(t *testing.T, srv http.Handler, name string, args map[string]any) map[string]any { t.Helper() bodyBytes, err := json.Marshal(map[string]any{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": map[string]any{"name": name, "arguments": args}, }) require.NoError(t, err) req := httptest.NewRequest(http.MethodPost, "/mcp", bytes.NewReader(bodyBytes)) rr := httptest.NewRecorder() srv.ServeHTTP(rr, req) require.Equal(t, http.StatusOK, rr.Code) var resp map[string]any require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &resp)) return resp } func TestBrainQueryReturnsResults(t *testing.T) { brainDir := t.TempDir() knowledge := filepath.Join(brainDir, "knowledge") require.NoError(t, os.MkdirAll(knowledge, 0o755)) require.NoError(t, os.WriteFile( filepath.Join(knowledge, "tdd.md"), []byte("# TDD\n\nTest-driven development is iterative.\n"), 0o644, )) srv := mcp.NewServer(brainDir, nil, nil) resp := toolCall(t, srv, "brain_query", map[string]any{"query": "tdd"}) require.Nil(t, resp["error"]) result := resp["result"].(map[string]any) content := result["content"].([]any) require.NotEmpty(t, content) text := content[0].(map[string]any)["text"].(string) assert.Contains(t, text, "tdd.md") } func TestBrainWriteCreatesFile(t *testing.T) { brainDir := t.TempDir() srv := mcp.NewServer(brainDir, nil, nil) resp := toolCall(t, srv, "brain_write", map[string]any{ "content": "# Test\n\nbody", "filename": "test.md", "type": "note", "domain": "personal", }) require.Nil(t, resp["error"]) got, err := os.ReadFile(filepath.Join(brainDir, "knowledge", "test.md")) require.NoError(t, err) assert.Contains(t, string(got), "type: note") assert.Contains(t, string(got), "domain: personal") assert.Contains(t, string(got), "# Test") } func TestBrainWriteRejectsTraversal(t *testing.T) { brainDir := t.TempDir() srv := mcp.NewServer(brainDir, nil, nil) resp := toolCall(t, srv, "brain_write", map[string]any{ "content": "x", "filename": "../escape.md", }) require.NotNil(t, resp["error"]) } func TestBrainWriteAcceptsDoubleDotInName(t *testing.T) { brainDir := t.TempDir() srv := mcp.NewServer(brainDir, nil, nil) resp := toolCall(t, srv, "brain_write", map[string]any{ "content": "x", "filename": "notes..draft.md", }) require.Nil(t, resp["error"]) _, err := os.Stat(filepath.Join(brainDir, "knowledge", "notes..draft.md")) require.NoError(t, err, "filename with embedded .. should be allowed") } func TestBrainIngestRawDryRun(t *testing.T) { brainDir := t.TempDir() require.NoError(t, os.MkdirAll(filepath.Join(brainDir, "wiki", "concepts"), 0o755)) srv := mcp.NewServer(brainDir, nil, nil) resp := toolCall(t, srv, "brain_ingest_raw", map[string]any{ "source": "test-source", "dry_run": true, "pages": []map[string]any{ { "title": "Test Concept", "type": "concept", "content": "## Definition\nA test concept.", }, }, }) require.Nil(t, resp["error"]) result := resp["result"].(map[string]any) content := result["content"].([]any) text := content[0].(map[string]any)["text"].(string) var parsed struct { Pages []string `json:"pages"` } require.NoError(t, json.Unmarshal([]byte(text), &parsed)) require.NotEmpty(t, parsed.Pages, "expected at least one page path") assert.Contains(t, parsed.Pages[0], "wiki/concepts/test-concept.md") // dry_run: no file should exist _, err := os.Stat(filepath.Join(brainDir, "wiki", "concepts", "test-concept.md")) assert.True(t, os.IsNotExist(err)) } func TestBrainIngestRejectsBoth(t *testing.T) { brainDir := t.TempDir() srv := mcp.NewServer(brainDir, nil, nil) resp := toolCall(t, srv, "brain_ingest", map[string]any{ "content": "x", "source": "y", "path": "/tmp/foo.md", }) require.NotNil(t, resp["error"]) } func TestBrainIngestRequiresOne(t *testing.T) { brainDir := t.TempDir() srv := mcp.NewServer(brainDir, nil, nil) resp := toolCall(t, srv, "brain_ingest", map[string]any{}) require.NotNil(t, resp["error"]) } func TestBrainIngestRejectsContentWithoutSource(t *testing.T) { brainDir := t.TempDir() srv := mcp.NewServer(brainDir, nil, nil) resp := toolCall(t, srv, "brain_ingest", map[string]any{ "content": "x", }) require.NotNil(t, resp["error"]) } func TestBrainIngestRequiresLLMConfigured(t *testing.T) { brainDir := t.TempDir() srv := mcp.NewServer(brainDir, nil, nil) // nil pipelineCfg → no LLM resp := toolCall(t, srv, "brain_ingest", map[string]any{ "content": "some content", "source": "test", }) require.NotNil(t, resp["error"]) errObj := resp["error"].(map[string]any) assert.Contains(t, errObj["message"].(string), "LLM not configured") } func TestSessionLogAppends(t *testing.T) { brainDir := t.TempDir() srv := mcp.NewServer(brainDir, nil, nil) resp := toolCall(t, srv, "session_log", map[string]any{ "session_id": "session-x", "skill": "tdd", "phase": "red", "final_status": "ok", }) require.Nil(t, resp["error"]) got, err := os.ReadFile(filepath.Join(brainDir, "sessions", "session-x.jsonl")) require.NoError(t, err) assert.Contains(t, string(got), `"skill":"tdd"`) assert.Contains(t, string(got), `"phase":"red"`) } func TestSessionLogRequiresSessionID(t *testing.T) { srv := mcp.NewServer(t.TempDir(), nil, nil) resp := toolCall(t, srv, "session_log", map[string]any{"skill": "tdd"}) require.NotNil(t, resp["error"]) }