feat(gitea): IssueTracker client + inject into brain server (#52)
Implements the IssueTracker port as a real Gitea REST client (#49c) — the new outbound dependency the brain server gains for capture. - CreateIssue / CommentIssue / CloseIssue(+optional closing comment) over the Gitea API. Owner is the const "mathias", never caller-supplied, so a caller cannot redirect a write to another owner's repo. - Token read once at construction (BRAIN_GITEA_TOKEN), held in the struct, travels only in the Authorization header — never logged or in argv. Error messages carry status + truncated body, never the token (regression-tested). gitea.New returns nil when URL or token is unset, so missing config = tracker disabled via one nil check. - Injected into the MCP server behind the capture.IssueTracker interface via WithIssueTracker (constructor injection, swappable/testable); main wires it from BRAIN_GITEA_URL (default https://git.d-ma.be) + BRAIN_GITEA_TOKEN. Consumed by the capture use-case in #53. Tests use httptest transports: create (owner+auth header asserted), comment, close with/without comment, error path that proves the token never leaks into an error string. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/mathiasbq/hyperguild/ingestion/internal/brainstore"
|
||||
"github.com/mathiasbq/hyperguild/ingestion/internal/capture"
|
||||
"github.com/mathiasbq/hyperguild/ingestion/internal/graphstore"
|
||||
"github.com/mathiasbq/hyperguild/ingestion/internal/graphsync"
|
||||
"github.com/mathiasbq/hyperguild/ingestion/internal/pipeline"
|
||||
@@ -48,6 +49,7 @@ type Server struct {
|
||||
embedder search.Embedder // nil = BM25-only retrieval
|
||||
graph graphsync.Store // nil = brain_graph and GraphRAG augmentation disabled
|
||||
store *brainstore.Store // shared brain write/update/get impl (also used by capture)
|
||||
tracker capture.IssueTracker // nil = no Gitea ticket integration; wired for capture (#53)
|
||||
}
|
||||
|
||||
// NewServer constructs a Server bound to brainDir. pipelineCfg supplies the
|
||||
@@ -100,6 +102,20 @@ func (s *Server) WithGraph(g *graphstore.PGStore) *Server {
|
||||
return s
|
||||
}
|
||||
|
||||
// WithIssueTracker injects the Gitea ticket tracker behind the
|
||||
// capture.IssueTracker interface. nil leaves ticket integration off. The
|
||||
// use-case (capture) consumes this in #53; it is wired here so the
|
||||
// dependency is constructed once and stays swappable/testable.
|
||||
func (s *Server) WithIssueTracker(t capture.IssueTracker) *Server {
|
||||
s.tracker = t
|
||||
return s
|
||||
}
|
||||
|
||||
// IssueTracker returns the injected ticket tracker (nil when unconfigured).
|
||||
func (s *Server) IssueTracker() capture.IssueTracker {
|
||||
return s.tracker
|
||||
}
|
||||
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// MCP streamable HTTP: GET establishes the SSE stream for server-to-client events.
|
||||
if r.Method == http.MethodGet {
|
||||
|
||||
@@ -2,12 +2,14 @@ package mcp_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mathiasbq/hyperguild/ingestion/internal/capture"
|
||||
"github.com/mathiasbq/hyperguild/ingestion/internal/mcp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -93,3 +95,22 @@ func TestServerUnknownMethodReturnsError(t *testing.T) {
|
||||
assert.Equal(t, float64(-32601), errObj["code"])
|
||||
assert.Contains(t, errObj["message"].(string), "unknown/method")
|
||||
}
|
||||
|
||||
type stubTracker struct{}
|
||||
|
||||
func (stubTracker) CreateIssue(context.Context, string, string, string) (capture.IssueRef, error) {
|
||||
return capture.IssueRef{}, nil
|
||||
}
|
||||
func (stubTracker) CloseIssue(context.Context, string, int, string) (capture.IssueRef, error) {
|
||||
return capture.IssueRef{}, nil
|
||||
}
|
||||
func (stubTracker) CommentIssue(context.Context, string, int, string) (capture.IssueRef, error) {
|
||||
return capture.IssueRef{}, nil
|
||||
}
|
||||
|
||||
func TestWithIssueTrackerInjects(t *testing.T) {
|
||||
srv := mcp.NewServer(t.TempDir(), nil, nil, nil)
|
||||
assert.Nil(t, srv.IssueTracker(), "tracker is off by default")
|
||||
srv = srv.WithIssueTracker(stubTracker{})
|
||||
assert.NotNil(t, srv.IssueTracker(), "tracker injected behind the interface")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user