Adds a two-dimensional address (wing, hall) to brain notes. A wing is a
topic domain (e.g. jepa-fx, hyperguild); a hall is one of a closed
vocabulary of memory types (facts, decisions, failures, hypotheses,
sources). Notes route to brain/wiki/<wing>/<hall>/<slug>.md with
wing/hall/created_at YAML frontmatter, making the directory a valid
Obsidian vault.
Changes:
- new package ingestion/internal/brain (NotePath, ValidHalls, Sanitise,
BuildWingIndex, BuildAllWingIndexes)
- api.WriteNote refactored to WriteNoteOptions; wing+hall routes to
brain/wiki/, otherwise falls back to brain/knowledge/ (legacy)
- search.Query → QueryOptions with optional Wing/Hall filtering; Result
carries wing/hall extracted from frontmatter or path segments
- MCP tools brain_write and brain_query gain optional wing/hall params
(hall enum-validated); new brain_index tool regenerates _index.md MOC
- POST /index REST endpoint mirrors brain_index
- brain_write auto-rebuilds the wing's _index.md after a wing+hall write
- scripts/migrate-brain-halls.sh migrates flat brain/wiki/{concepts,entities}/
into the new layout (dry-run by default, --commit applies)
All existing tests pass; new tests cover wing/hall write routing, scope
filtering, invalid hall rejection, _index.md generation, and migration
script paths.
Closes hyperguild#1.
93 lines
2.7 KiB
Go
93 lines
2.7 KiB
Go
package mcp_test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/mathiasbq/hyperguild/ingestion/internal/mcp"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func body(t *testing.T, v any) *bytes.Buffer {
|
|
t.Helper()
|
|
b, err := json.Marshal(v)
|
|
require.NoError(t, err)
|
|
return bytes.NewBuffer(b)
|
|
}
|
|
|
|
func TestServerInitialize(t *testing.T) {
|
|
srv := mcp.NewServer(t.TempDir(), nil, nil, nil)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/mcp", body(t, map[string]any{
|
|
"jsonrpc": "2.0", "id": 1, "method": "initialize",
|
|
"params": map[string]any{},
|
|
}))
|
|
rr := httptest.NewRecorder()
|
|
srv.ServeHTTP(rr, req)
|
|
|
|
assert.Equal(t, http.StatusOK, rr.Code)
|
|
var resp map[string]any
|
|
require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &resp))
|
|
result := resp["result"].(map[string]any)
|
|
assert.Equal(t, "2024-11-05", result["protocolVersion"])
|
|
}
|
|
|
|
func TestServerToolsList(t *testing.T) {
|
|
srv := mcp.NewServer(t.TempDir(), nil, nil, nil)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/mcp", body(t, map[string]any{
|
|
"jsonrpc": "2.0", "id": 2, "method": "tools/list",
|
|
}))
|
|
rr := httptest.NewRecorder()
|
|
srv.ServeHTTP(rr, req)
|
|
|
|
assert.Equal(t, http.StatusOK, rr.Code)
|
|
var resp map[string]any
|
|
require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &resp))
|
|
tools := resp["result"].(map[string]any)["tools"].([]any)
|
|
names := make([]string, 0, len(tools))
|
|
for _, t := range tools {
|
|
names = append(names, t.(map[string]any)["name"].(string))
|
|
}
|
|
assert.ElementsMatch(t, []string{
|
|
"brain_query", "brain_write", "brain_index", "brain_ingest_raw", "brain_ingest",
|
|
"brain_answer", "brain_classify", "session_log",
|
|
}, names)
|
|
}
|
|
|
|
func TestServerNotificationGetsNoBody(t *testing.T) {
|
|
srv := mcp.NewServer(t.TempDir(), nil, nil, nil)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/mcp", body(t, map[string]any{
|
|
"jsonrpc": "2.0", "method": "notifications/initialized",
|
|
}))
|
|
rr := httptest.NewRecorder()
|
|
srv.ServeHTTP(rr, req)
|
|
|
|
assert.Equal(t, http.StatusOK, rr.Code)
|
|
assert.Empty(t, strings.TrimSpace(rr.Body.String()))
|
|
}
|
|
|
|
func TestServerUnknownMethodReturnsError(t *testing.T) {
|
|
srv := mcp.NewServer(t.TempDir(), nil, nil, nil)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/mcp", body(t, map[string]any{
|
|
"jsonrpc": "2.0", "id": 3, "method": "unknown/method",
|
|
}))
|
|
rr := httptest.NewRecorder()
|
|
srv.ServeHTTP(rr, req)
|
|
|
|
assert.Equal(t, http.StatusOK, rr.Code)
|
|
var resp map[string]any
|
|
require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &resp))
|
|
require.NotNil(t, resp["error"])
|
|
errObj := resp["error"].(map[string]any)
|
|
assert.Equal(t, float64(-32601), errObj["code"])
|
|
assert.Contains(t, errObj["message"].(string), "unknown/method")
|
|
}
|