Wires Config → LiteLLMExecutor → Router → four skills (review, debug, retrospective, trainer) → Registry → MCP server with bearer auth and /healthz. Each skill's CompleteFunc is wrapped so the Router decides local-vs-Claude per call and logs every decision to the brain /mcp. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
124 lines
3.6 KiB
Go
124 lines
3.6 KiB
Go
package main_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os/exec"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestRoutingPodEndToEnd boots the binary against fake LiteLLM + brain servers,
|
|
// calls tools/list and one tools/call, and verifies the brain saw a session_log POST.
|
|
func TestRoutingPodEndToEnd(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("end-to-end binary boot")
|
|
}
|
|
|
|
var brainHits int
|
|
llm := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"choices": []map[string]any{{"message": map[string]any{"role": "assistant", "content": "stub"}}},
|
|
})
|
|
}))
|
|
defer llm.Close()
|
|
|
|
brain := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/pass-rate":
|
|
brainHits++
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"pass_rate": 0.95})
|
|
case "/mcp":
|
|
brainHits++
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"jsonrpc": "2.0", "id": 1, "result": map[string]any{}})
|
|
}
|
|
}))
|
|
defer brain.Close()
|
|
|
|
bin := buildRouting(t)
|
|
cmd := exec.Command(bin)
|
|
cmd.Env = append(cmd.Env,
|
|
"ROUTING_PORT=33310",
|
|
"LITELLM_BASE_URL="+llm.URL,
|
|
"LITELLM_API_KEY=stub",
|
|
"BRAIN_URL="+brain.URL,
|
|
"SUPERVISOR_CONFIG_DIR=../../config/supervisor",
|
|
"PATH="+osPath(),
|
|
)
|
|
require.NoError(t, cmd.Start())
|
|
t.Cleanup(func() { _ = cmd.Process.Kill() })
|
|
|
|
require.NoError(t, waitForPort(t, "127.0.0.1:33310", 5*time.Second))
|
|
|
|
resp := mcpCall(t, "http://127.0.0.1:33310/mcp", `{"jsonrpc":"2.0","id":1,"method":"tools/list"}`)
|
|
assert.Contains(t, resp, `"review"`)
|
|
assert.Contains(t, resp, `"debug"`)
|
|
assert.Contains(t, resp, `"retrospective"`)
|
|
assert.Contains(t, resp, `"trainer"`)
|
|
|
|
resp = mcpCall(t, "http://127.0.0.1:33310/mcp", `{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"review","arguments":{"project_root":"/tmp","files":["README.md"]}}}`)
|
|
_ = resp // shape varies by skill; we only need a 200
|
|
|
|
// Wait briefly for the async session_log to land.
|
|
deadline := time.Now().Add(2 * time.Second)
|
|
for time.Now().Before(deadline) && brainHits < 2 {
|
|
time.Sleep(50 * time.Millisecond)
|
|
}
|
|
assert.GreaterOrEqual(t, brainHits, 2, "expected at least one /pass-rate hit and one /mcp session_log hit")
|
|
}
|
|
|
|
func buildRouting(t *testing.T) string {
|
|
t.Helper()
|
|
bin := t.TempDir() + "/routing"
|
|
out, err := exec.Command("go", "build", "-o", bin, "github.com/mathiasbq/supervisor/cmd/routing").CombinedOutput()
|
|
require.NoError(t, err, "build failed: %s", out)
|
|
return bin
|
|
}
|
|
|
|
func waitForPort(_ *testing.T, addr string, dur time.Duration) error {
|
|
deadline := time.Now().Add(dur)
|
|
for time.Now().Before(deadline) {
|
|
c, err := http.Get("http://" + addr + "/healthz") //nolint:noctx
|
|
if err == nil {
|
|
_ = c.Body.Close()
|
|
return nil
|
|
}
|
|
conn, err := http.NewRequest(http.MethodPost, "http://"+addr+"/mcp", strings.NewReader(`{}`))
|
|
if err == nil {
|
|
r, err := http.DefaultClient.Do(conn)
|
|
if err == nil {
|
|
_ = r.Body.Close()
|
|
return nil
|
|
}
|
|
}
|
|
time.Sleep(50 * time.Millisecond)
|
|
}
|
|
return context.DeadlineExceeded
|
|
}
|
|
|
|
func mcpCall(t *testing.T, url, body string) string {
|
|
t.Helper()
|
|
r, err := http.Post(url, "application/json", strings.NewReader(body)) //nolint:noctx
|
|
require.NoError(t, err)
|
|
defer func() { _ = r.Body.Close() }()
|
|
raw, err := io.ReadAll(r.Body)
|
|
require.NoError(t, err)
|
|
return string(raw)
|
|
}
|
|
|
|
func osPath() string {
|
|
for _, e := range append([]string{}, exec.Command("env").Env...) {
|
|
if strings.HasPrefix(e, "PATH=") {
|
|
return strings.TrimPrefix(e, "PATH=")
|
|
}
|
|
}
|
|
return "/usr/bin:/bin"
|
|
}
|