package mcp_test import ( "context" "encoding/json" "os" "path/filepath" "strings" "testing" "time" "github.com/mathiasbq/hyperguild/ingestion/internal/mcp" "github.com/mathiasbq/hyperguild/ingestion/internal/vectorstore" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // callResult parses the JSON text payload of a successful tool call. func callResult(t *testing.T, resp map[string]any) map[string]any { t.Helper() require.Nil(t, resp["error"], "tool returned error: %v", resp["error"]) text := resp["result"].(map[string]any)["content"].([]any)[0].(map[string]any)["text"].(string) var out map[string]any require.NoError(t, json.Unmarshal([]byte(text), &out)) return out } func TestBrainUpdateSupersedesExisting(t *testing.T) { brainDir := t.TempDir() srv := mcp.NewServer(brainDir, nil, nil, nil) // Seed via brain_write so the note carries real frontmatter. callResult(t, toolCall(t, srv, "brain_write", map[string]any{ "content": "# Old\n\nold body\n", "filename": "val-vol", "wing": "jepa-fx", "hall": "facts", })) out := callResult(t, toolCall(t, srv, "brain_update", map[string]any{ "wing": "jepa-fx", "hall": "facts", "slug": "val-vol", "content": "# New\n\nnew body\n", "reason": "facts changed", })) assert.Equal(t, "wiki/jepa-fx/facts/val-vol.md", out["path"]) assert.Equal(t, out["path"], out["id"]) assert.NotEmpty(t, out["content_hash"]) assert.Equal(t, true, out["superseded"]) got, err := os.ReadFile(filepath.Join(brainDir, "wiki/jepa-fx/facts/val-vol.md")) require.NoError(t, err) s := string(got) assert.Contains(t, s, "# New") assert.NotContains(t, s, "old body") assert.Contains(t, s, "wing: jepa-fx") assert.Contains(t, s, "supersede_reason: facts changed") assert.Contains(t, s, "supersedes:") } func TestBrainUpdateMissingTargetErrorsNoCreate(t *testing.T) { brainDir := t.TempDir() srv := mcp.NewServer(brainDir, nil, nil, nil) resp := toolCall(t, srv, "brain_update", map[string]any{ "wing": "jepa-fx", "hall": "facts", "slug": "ghost", "content": "x\n", }) require.NotNil(t, resp["error"]) assert.Contains(t, resp["error"].(map[string]any)["message"].(string), "does not exist") _, statErr := os.Stat(filepath.Join(brainDir, "wiki/jepa-fx/facts/ghost.md")) assert.True(t, os.IsNotExist(statErr)) } func TestBrainUpdateByFullPath(t *testing.T) { brainDir := t.TempDir() srv := mcp.NewServer(brainDir, nil, nil, nil) callResult(t, toolCall(t, srv, "brain_write", map[string]any{ "content": "old\n", "filename": "n", "wing": "a", "hall": "facts", })) out := callResult(t, toolCall(t, srv, "brain_update", map[string]any{ "slug": "wiki/a/facts/n.md", "content": "fresh\n", })) assert.Equal(t, "wiki/a/facts/n.md", out["path"]) } func TestBrainGetByIDAndPath(t *testing.T) { brainDir := t.TempDir() srv := mcp.NewServer(brainDir, nil, nil, nil) w := callResult(t, toolCall(t, srv, "brain_write", map[string]any{ "content": "# Body\n\ntext\n", "filename": "n", "wing": "a", "hall": "facts", })) id := w["id"].(string) hash := w["content_hash"].(string) require.NotEmpty(t, id) require.NotEmpty(t, hash) // by id g1 := callResult(t, toolCall(t, srv, "brain_get", map[string]any{"id": id})) assert.Equal(t, id, g1["path"]) assert.Equal(t, hash, g1["content_hash"], "content_hash must round-trip write→get") assert.Contains(t, g1["body"].(string), "# Body") fm := g1["frontmatter"].(map[string]any) assert.Equal(t, "a", fm["wing"]) // by path g2 := callResult(t, toolCall(t, srv, "brain_get", map[string]any{"path": id})) assert.Equal(t, hash, g2["content_hash"]) } func TestBrainGetMissingArgsErrors(t *testing.T) { srv := mcp.NewServer(t.TempDir(), nil, nil, nil) resp := toolCall(t, srv, "brain_get", map[string]any{}) require.NotNil(t, resp["error"]) } func TestBrainWriteReturnsHandle(t *testing.T) { brainDir := t.TempDir() srv := mcp.NewServer(brainDir, nil, nil, nil) out := callResult(t, toolCall(t, srv, "brain_write", map[string]any{ "content": "# X\n\nbody\n", "filename": "x", "wing": "a", "hall": "facts", })) assert.Equal(t, "wiki/a/facts/x.md", out["path"]) assert.Equal(t, out["path"], out["id"]) assert.NotEmpty(t, out["content_hash"]) } // --- retrieval-reflects-new-content: exercises the real mtime-driven Sync --- type fakeVecStore struct { chunks map[string][]float32 deleted []string } func (f *fakeVecStore) KnownPathsWithTime(_ context.Context) (map[string]time.Time, error) { m := make(map[string]time.Time, len(f.chunks)) for p := range f.chunks { m[p] = time.Unix(0, 0) // always stale → mtime(now) is always newer } return m, nil } func (f *fakeVecStore) Upsert(_ context.Context, path string, vec []float32) error { f.chunks[path] = vec return nil } func (f *fakeVecStore) Delete(_ context.Context, path string) error { delete(f.chunks, path) f.deleted = append(f.deleted, path) return nil } type fakeEmbedder struct{ seen []string } func (e *fakeEmbedder) Embed(_ context.Context, text string) ([]float32, error) { e.seen = append(e.seen, text) return []float32{1, 0, 0}, nil } // TestBrainUpdateReembedsNewContent proves the supersede contract end to // end against the actual embedding mechanism: brain_update rewrites the // file, advancing its mtime, and the next vectorstore.Sync pass re-embeds // the NEW body and drops the stale chunk. No stub of the re-index path. func TestBrainUpdateReembedsNewContent(t *testing.T) { brainDir := t.TempDir() srv := mcp.NewServer(brainDir, nil, nil, nil) ctx := context.Background() callResult(t, toolCall(t, srv, "brain_write", map[string]any{ "content": "# Note\n\nthe OLD distinctive payload\n", "filename": "n", "wing": "a", "hall": "facts", })) store := &fakeVecStore{chunks: map[string][]float32{}} emb := &fakeEmbedder{} // First sync embeds the original content. _, err := vectorstore.Sync(ctx, brainDir, store, emb) require.NoError(t, err) require.NotEmpty(t, store.chunks) require.True(t, anyContains(emb.seen, "OLD distinctive payload")) callResult(t, toolCall(t, srv, "brain_update", map[string]any{ "wing": "a", "hall": "facts", "slug": "n", "content": "# Note\n\nthe NEW distinctive payload\n", })) emb.seen = nil // only watch what the second pass embeds _, err = vectorstore.Sync(ctx, brainDir, store, emb) require.NoError(t, err) assert.True(t, anyContains(emb.seen, "NEW distinctive payload"), "Sync must re-embed the superseded body; saw %v", emb.seen) assert.False(t, anyContains(emb.seen, "OLD distinctive payload"), "the old body must not be re-embedded") assert.NotEmpty(t, store.deleted, "stale chunks must be deleted before re-embed") } func anyContains(ss []string, sub string) bool { for _, s := range ss { if strings.Contains(s, sub) { return true } } return false }