feat(routing): cmd/routing binary
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>
This commit is contained in:
123
cmd/routing/main_test.go
Normal file
123
cmd/routing/main_test.go
Normal file
@@ -0,0 +1,123 @@
|
||||
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"
|
||||
}
|
||||
Reference in New Issue
Block a user