Files
hyperguild/ingestion/internal/graphsync/graphsync_test.go
Mathias f43e0bccbf
All checks were successful
CI / Lint / Test / Vet (push) Successful in 13s
CI / Mirror to GitHub (push) Successful in 4s
feat(graph): wire graphsync into MCP write/ingest/tunnel handlers
Commit 2 of Track A. Service stays a no-op until BRAIN_GRAPH_ENABLED=
true; flipping it on creates the schema (idempotent), starts indexing
every successful write, and optionally backfills the existing brain
dir.

- internal/graphsync: best-effort wrapper around graph.Extract +
  graphstore. IndexDoc reads docPath under brainDir, parses, upserts
  entity + replaces edges. BackfillFromBrainDir walks wiki/ +
  knowledge/. Both are no-ops on nil store so callers wire
  unconditionally.

- mcp.Server gains WithGraph builder + graphsync.Store field.
  brain_write, brain_ingest, brain_ingest_raw, brain_tunnel call
  indexInGraph after success — failures slog.Warn but never
  propagate (graph is augmentation, not correctness).

- cmd/server gates the wiring on BRAIN_GRAPH_ENABLED=true (default
  off so first rollout doesn't surprise). BRAIN_GRAPH_BACKFILL=true
  triggers a one-shot walk of the brain dir on boot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 15:21:33 +02:00

135 lines
3.7 KiB
Go

package graphsync
import (
"context"
"errors"
"os"
"path/filepath"
"sync"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mathiasbq/hyperguild/ingestion/internal/graph"
)
// fakeStore captures the calls IndexDoc / BackfillFromBrainDir made.
type fakeStore struct {
mu sync.Mutex
upserts []graph.Entity
replaces map[string][]graph.Edge
deletes []string
failOn string // upsert fails when entity slug == failOn
}
func newFakeStore() *fakeStore {
return &fakeStore{replaces: make(map[string][]graph.Edge)}
}
func (f *fakeStore) UpsertEntity(_ context.Context, e graph.Entity) error {
f.mu.Lock()
defer f.mu.Unlock()
if f.failOn != "" && e.Slug == f.failOn {
return errors.New("synthetic failure")
}
f.upserts = append(f.upserts, e)
return nil
}
func (f *fakeStore) ReplaceEdgesForDoc(_ context.Context, docPath string, edges []graph.Edge) error {
f.mu.Lock()
defer f.mu.Unlock()
f.replaces[docPath] = append([]graph.Edge(nil), edges...)
return nil
}
func (f *fakeStore) DeleteByDoc(_ context.Context, docPath string) error {
f.mu.Lock()
defer f.mu.Unlock()
f.deletes = append(f.deletes, docPath)
return nil
}
func writeBrain(t *testing.T, brainDir, relPath, body string) {
t.Helper()
full := filepath.Join(brainDir, filepath.FromSlash(relPath))
require.NoError(t, os.MkdirAll(filepath.Dir(full), 0o755))
require.NoError(t, os.WriteFile(full, []byte(body), 0o644))
}
func TestIndexDoc_UpsertsEntityAndEdges(t *testing.T) {
tmp := t.TempDir()
writeBrain(t, tmp, "wiki/concepts/foo.md", `---
title: Foo
---
# Foo
Linking to [[bar]] and [[baz|Baz]].
`)
fs := newFakeStore()
require.NoError(t, IndexDoc(context.Background(), fs, tmp, "wiki/concepts/foo.md"))
require.Len(t, fs.upserts, 1)
assert.Equal(t, "foo", fs.upserts[0].Slug)
assert.Equal(t, "concept", fs.upserts[0].Type)
edges := fs.replaces["wiki/concepts/foo.md"]
require.Len(t, edges, 2)
assert.Equal(t, "bar", edges[0].DstSlug)
assert.Equal(t, "baz", edges[1].DstSlug)
}
func TestIndexDoc_NoopOnNilStore(t *testing.T) {
require.NoError(t, IndexDoc(context.Background(), nil, "anywhere", "foo.md"))
}
func TestIndexDoc_NoopOnEmptyRelPath(t *testing.T) {
fs := newFakeStore()
require.NoError(t, IndexDoc(context.Background(), fs, "anywhere", ""))
assert.Empty(t, fs.upserts)
}
func TestIndexDoc_ErrorsOnMissingFile(t *testing.T) {
fs := newFakeStore()
err := IndexDoc(context.Background(), fs, t.TempDir(), "wiki/nope.md")
require.Error(t, err)
}
func TestIndexDoc_SurfacesStoreFailure(t *testing.T) {
tmp := t.TempDir()
writeBrain(t, tmp, "wiki/concepts/boom.md", "# Boom\n")
fs := newFakeStore()
fs.failOn = "boom"
err := IndexDoc(context.Background(), fs, tmp, "wiki/concepts/boom.md")
require.Error(t, err)
}
func TestBackfillFromBrainDir_WalksWikiAndKnowledge(t *testing.T) {
tmp := t.TempDir()
writeBrain(t, tmp, "wiki/concepts/foo.md", "# Foo\n[[bar]]\n")
writeBrain(t, tmp, "wiki/entities/bar.md", "# Bar\n")
writeBrain(t, tmp, "knowledge/legacy.md", "# Legacy [[foo]]\n")
// non-markdown file should be skipped
writeBrain(t, tmp, "wiki/concepts/skip.txt", "ignore me")
fs := newFakeStore()
n, err := BackfillFromBrainDir(context.Background(), fs, tmp)
require.NoError(t, err)
assert.Equal(t, 3, n)
assert.Len(t, fs.upserts, 3)
}
func TestBackfillFromBrainDir_TolerantOfMissingDirs(t *testing.T) {
tmp := t.TempDir()
fs := newFakeStore()
n, err := BackfillFromBrainDir(context.Background(), fs, tmp)
require.NoError(t, err)
assert.Equal(t, 0, n)
}
func TestBackfillFromBrainDir_NilStoreNoop(t *testing.T) {
n, err := BackfillFromBrainDir(context.Background(), nil, t.TempDir())
require.NoError(t, err)
assert.Equal(t, 0, n)
}