updates on the ai and agent and all thse things awith onboarding
This commit is contained in:
190
routes/routes_ai_registry_test.go
Normal file
190
routes/routes_ai_registry_test.go
Normal file
@@ -0,0 +1,190 @@
|
||||
package routes_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"doormile/controllers"
|
||||
"doormile/internal/ai/playground"
|
||||
"doormile/utils"
|
||||
)
|
||||
|
||||
// The AI agent registry's gates, over real HTTP (see routes_logistics_test.go
|
||||
// for what this style of test does and does not prove). Every refusal below
|
||||
// happens in middleware, before a handler could touch the database.
|
||||
|
||||
func tenantToken(t *testing.T, userID, roleID, tenantID int) string {
|
||||
t.Helper()
|
||||
tok, err := utils.GenerateToken(userID, "client@partner.example", roleID, tenantID, 1001, jwtSecret)
|
||||
if err != nil {
|
||||
t.Fatalf("could not mint a tenant token: %v", err)
|
||||
}
|
||||
return tok
|
||||
}
|
||||
|
||||
var aiReads = []string{
|
||||
"/api/v1/admin/ai/agents",
|
||||
"/api/v1/admin/ai/agents/EXCEPTION_AGENT",
|
||||
"/api/v1/admin/ai/skills",
|
||||
"/api/v1/admin/ai/tools",
|
||||
"/api/v1/admin/ai/audit",
|
||||
"/api/v1/admin/ai/insights",
|
||||
"/api/v1/admin/ai/insights?days=30",
|
||||
"/api/v1/admin/ai/decisions",
|
||||
}
|
||||
|
||||
var aiWrites = []struct{ method, path, body string }{
|
||||
{http.MethodPatch, "/api/v1/admin/ai/skills/skill_sla_guardian", `{"enabled":false}`},
|
||||
{http.MethodPost, "/api/v1/admin/ai/skills", `{"agentid":"CONSOLE_OPS_AGENT","title":"x","tools":["scan_bookings"]}`},
|
||||
{http.MethodPatch, "/api/v1/admin/ai/agents/EXCEPTION_AGENT", `{"autonomous":true,"confirm":"EXCEPTION_AGENT"}`},
|
||||
{http.MethodPost, "/api/v1/admin/ai/playground/run", `{"agentid":"EXCEPTION_AGENT","prompt":"hi"}`},
|
||||
}
|
||||
|
||||
func TestAIRegistryRequiresALogin(t *testing.T) {
|
||||
app := newApp()
|
||||
for _, p := range aiReads {
|
||||
if code, _ := do(t, app, http.MethodGet, p, "", ""); code != http.StatusUnauthorized {
|
||||
t.Errorf("GET %s with no token = %d, want 401", p, code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAIRegistryRefusesNonConsoleRoles(t *testing.T) {
|
||||
app := newApp()
|
||||
for _, role := range []int{5, 6, 9} { // miler, hub staff, customer
|
||||
for _, p := range aiReads {
|
||||
if code, _ := do(t, app, http.MethodGet, p, token(t, 1, role), ""); code != http.StatusForbidden {
|
||||
t.Errorf("role %d GET %s = %d, want 403", role, p, code)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A partner-tenant login is refused outright — even an admin-role one — rather
|
||||
// than shown an empty registry.
|
||||
func TestAIRegistryRefusesPartnerTenantLogins(t *testing.T) {
|
||||
app := newApp()
|
||||
tok := tenantToken(t, 50, 1, 7)
|
||||
for _, p := range aiReads {
|
||||
code, body := do(t, app, http.MethodGet, p, tok, "")
|
||||
if code != http.StatusForbidden {
|
||||
t.Errorf("tenant GET %s = %d, want 403", p, code)
|
||||
}
|
||||
if code == http.StatusForbidden && !strings.Contains(body, "Doormile staff only") {
|
||||
t.Errorf("tenant GET %s refused with the wrong message: %s", p, body)
|
||||
}
|
||||
}
|
||||
for _, w := range aiWrites {
|
||||
if code, _ := do(t, app, w.method, w.path, tok, w.body); code != http.StatusForbidden {
|
||||
t.Errorf("tenant %s %s = %d, want 403", w.method, w.path, code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Managers (3) and executives (4) may read the registry but not change it.
|
||||
func TestAIRegistryWritesAreAdminOnly(t *testing.T) {
|
||||
app := newApp()
|
||||
for _, role := range []int{3, 4} {
|
||||
for _, w := range aiWrites {
|
||||
code, body := do(t, app, w.method, w.path, token(t, 1, role), w.body)
|
||||
if code != http.StatusForbidden {
|
||||
t.Errorf("role %d %s %s = %d, want 403", role, w.method, w.path, code)
|
||||
}
|
||||
if code == http.StatusForbidden && !strings.Contains(body, "insufficient permissions") {
|
||||
t.Errorf("role %d %s %s refused by the wrong gate: %s", role, w.method, w.path, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Doormile staff with roleid 1 get through every gate. There is no database
|
||||
// in this test, so the handler's first query panics and recover answers 500 —
|
||||
// which is the proof the route exists and nothing in front of it refused.
|
||||
func TestAIRegistryAdminPassesEveryGate(t *testing.T) {
|
||||
app := newApp()
|
||||
tok := token(t, 1, 1)
|
||||
for _, p := range aiReads {
|
||||
if code, _ := do(t, app, http.MethodGet, p, tok, ""); code == 401 || code == 403 || code == 404 {
|
||||
t.Errorf("admin GET %s = %d; a gate refused or the route is missing", p, code)
|
||||
}
|
||||
}
|
||||
for _, w := range aiWrites {
|
||||
if code, _ := do(t, app, w.method, w.path, tok, w.body); code == 401 || code == 403 || code == 404 {
|
||||
t.Errorf("admin %s %s = %d; a gate refused or the route is missing", w.method, w.path, code)
|
||||
}
|
||||
}
|
||||
// Read roles get through the read gates too.
|
||||
for _, role := range []int{3, 4} {
|
||||
if code, _ := do(t, app, http.MethodGet, "/api/v1/admin/ai/agents", token(t, 1, role), ""); code == 401 || code == 403 {
|
||||
t.Errorf("role %d was refused a registry read (%d)", role, code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInternalRegistryNeedsTheInternalKey(t *testing.T) {
|
||||
t.Setenv("INTERNAL_API_KEY", "engine-key-for-tests")
|
||||
app := newApp()
|
||||
for _, key := range []string{"", "wrong-key"} {
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/internal/ai/registry", nil)
|
||||
if key != "" {
|
||||
req.Header.Set("X-Internal-Key", key)
|
||||
}
|
||||
resp, err := app.Test(req, int(10*time.Second/time.Millisecond))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
t.Errorf("internal registry with key %q = %d, want 401", key, resp.StatusCode)
|
||||
}
|
||||
}
|
||||
// A console JWT is not an internal key.
|
||||
if code, _ := do(t, app, http.MethodGet, "/api/v1/internal/ai/registry", token(t, 1, 1), ""); code != http.StatusUnauthorized {
|
||||
t.Errorf("internal registry with an admin JWT = %d, want 401", code)
|
||||
}
|
||||
}
|
||||
|
||||
// The Test playground (Phase 6). With no model client wired the endpoint
|
||||
// says so plainly (503 with a code the console branches on) — it never
|
||||
// pretends to run. With one wired, bad input is refused before any database
|
||||
// or model call.
|
||||
func TestAIPlaygroundWithoutAClientIs503(t *testing.T) {
|
||||
app := newApp()
|
||||
prev := controllers.PlaygroundModel
|
||||
controllers.PlaygroundModel = nil
|
||||
defer func() { controllers.PlaygroundModel = prev }()
|
||||
|
||||
code, body := do(t, app, http.MethodPost, "/api/v1/admin/ai/playground/run", token(t, 1, 1),
|
||||
`{"agentid":"EXCEPTION_AGENT","prompt":"hi"}`)
|
||||
if code != http.StatusServiceUnavailable || !strings.Contains(body, "PLAYGROUND_NOT_CONFIGURED") {
|
||||
t.Fatalf("no client: %d %s, want 503 PLAYGROUND_NOT_CONFIGURED", code, body)
|
||||
}
|
||||
}
|
||||
|
||||
type noCallModel struct{ t *testing.T }
|
||||
|
||||
func (m noCallModel) Next(context.Context, playground.Request) (playground.Reply, error) {
|
||||
m.t.Fatal("the model was called for a request that should have been refused")
|
||||
return playground.Reply{}, nil
|
||||
}
|
||||
|
||||
func TestAIPlaygroundRefusesBadInput(t *testing.T) {
|
||||
app := newApp()
|
||||
prev := controllers.PlaygroundModel
|
||||
controllers.PlaygroundModel = noCallModel{t}
|
||||
defer func() { controllers.PlaygroundModel = prev }()
|
||||
|
||||
for _, b := range []string{
|
||||
`{"agentid":"EXCEPTION_AGENT","prompt":" "}`,
|
||||
`{"prompt":"hi"}`,
|
||||
`{"agentid":"EXCEPTION_AGENT","prompt":"` + strings.Repeat("x", 2001) + `"}`,
|
||||
`not json`,
|
||||
} {
|
||||
if code, body := do(t, app, http.MethodPost, "/api/v1/admin/ai/playground/run", token(t, 1, 1), b); code != http.StatusBadRequest {
|
||||
t.Errorf("body %.40q = %d %s, want 400", b, code, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user