agent
This commit is contained in:
@@ -17,4 +17,9 @@ func RegisterAssistantRoutes(api fiber.Router, f *facade.Facade) {
|
||||
|
||||
assistant.Get("/status", f.AssistantController.Status)
|
||||
assistant.Post("/ask", f.AssistantController.Ask)
|
||||
assistant.Post("/approve", f.AssistantController.Approve)
|
||||
|
||||
// The MCP door, under the same group so it inherits the same session guard.
|
||||
// One endpoint: JSON-RPC carries the method in the body.
|
||||
assistant.Post("/mcp", f.MCPController.Handle)
|
||||
}
|
||||
|
||||
114
routes/startup_test.go
Normal file
114
routes/startup_test.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"nearle/facade"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// Can this server be built and can its routes be reached?
|
||||
//
|
||||
// Everything else in this repository tests a function. This tests the thing
|
||||
// that actually happens on deploy: the whole object graph is constructed and
|
||||
// every route is registered. Nothing here needs a database — the repositories
|
||||
// hold their handle without touching it — so it runs in CI beside the unit
|
||||
// tests rather than in an environment somebody has to provision.
|
||||
//
|
||||
// ── Why it is worth its own file ────────────────────────────────────────────
|
||||
//
|
||||
// Three things in `NewFacade` PANIC rather than return an error: a tool that
|
||||
// fails to register, a help corpus that will not load, and an agent naming a
|
||||
// tool that does not exist. Each is a programming mistake that should stop a
|
||||
// deploy, and each was previously reachable only by starting the server against
|
||||
// a real database — which meant, in practice, by deploying.
|
||||
//
|
||||
// The agent one is not hypothetical: a typo in `agents/orders.yaml` is a file
|
||||
// edit away, and it takes a working assistant down at boot.
|
||||
|
||||
func testFacade(t *testing.T) *facade.Facade {
|
||||
t.Helper()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
// Rendered as a failure rather than a panicking test, because the
|
||||
// message IS the point — "agent orders lists a tool that does not
|
||||
// exist" is the whole diagnosis.
|
||||
t.Fatalf("the server cannot start: %v", r)
|
||||
}
|
||||
}()
|
||||
// No database, no catalogue, no embedder, no model. A deployment with none
|
||||
// of those must still boot and say what it is missing, rather than failing
|
||||
// somewhere the operator cannot see.
|
||||
return facade.NewFacade(nil, nil, nil, nil, "")
|
||||
}
|
||||
|
||||
func TestTheServerCanBeBuilt(t *testing.T) {
|
||||
f := testFacade(t)
|
||||
if f == nil {
|
||||
t.Fatal("no facade")
|
||||
}
|
||||
// The two doors onto the assistant. Absent means a route registered below
|
||||
// would nil-panic on its first request rather than at boot.
|
||||
if f.AssistantController == nil {
|
||||
t.Fatal("no assistant controller")
|
||||
}
|
||||
if f.MCPController == nil {
|
||||
t.Fatal("no MCP controller")
|
||||
}
|
||||
if f.Tools == nil {
|
||||
t.Fatal("no tool registry")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEveryAssistantRouteIsReachable(t *testing.T) {
|
||||
// Registered, not merely written down. A route added to a file that nothing
|
||||
// calls is invisible until somebody reports the feature missing.
|
||||
app := fiber.New()
|
||||
RegisterRoutes(app, testFacade(t))
|
||||
|
||||
for _, route := range []struct {
|
||||
method, path string
|
||||
}{
|
||||
{"GET", "/live/api/v1/web/assistant/status"},
|
||||
{"POST", "/live/api/v1/web/assistant/ask"},
|
||||
{"POST", "/live/api/v1/web/assistant/approve"},
|
||||
{"POST", "/live/api/v1/web/assistant/mcp"},
|
||||
} {
|
||||
req := httptest.NewRequest(route.method, route.path, strings.NewReader("{}"))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := app.Test(req, -1)
|
||||
if err != nil {
|
||||
t.Fatalf("%s %s: %v", route.method, route.path, err)
|
||||
}
|
||||
if resp.StatusCode == fiber.StatusNotFound {
|
||||
t.Fatalf("%s %s is not registered", route.method, route.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheAssistantSurfaceSitsBehindTheSessionGuard(t *testing.T) {
|
||||
// The assistant reads the same data the console does and must read it as
|
||||
// the same person. Being under `/v1/web` is what puts it behind WebAuth —
|
||||
// a route registered one path segment to the left would answer anybody.
|
||||
app := fiber.New()
|
||||
RegisterRoutes(app, testFacade(t))
|
||||
|
||||
// WEB_AUTH_REQUIRED is off by default, so an untokened request reaches the
|
||||
// handler; the assistant's own controller then refuses it. Either way it
|
||||
// must not answer with data.
|
||||
req := httptest.NewRequest("POST", "/live/api/v1/web/assistant/ask",
|
||||
strings.NewReader(`{"question":"what is stuck?"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := app.Test(req, -1)
|
||||
if err != nil {
|
||||
t.Fatalf("calling: %v", err)
|
||||
}
|
||||
if resp.StatusCode == fiber.StatusOK {
|
||||
t.Fatal("an untokened question was answered")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user