146 lines
4.7 KiB
Go
146 lines
4.7 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestAPIAppendRouteDecisionLogReturnsCreated(t *testing.T) {
|
|
handler := NewAPIHandler("secret-token", ActionSet{
|
|
AppendRouteDecisionLog: func(_ context.Context, req AppendRouteDecisionLogRequest) (RouteDecisionLogInfo, error) {
|
|
if req.RequestID != "req-1" {
|
|
t.Fatalf("RequestID = %q, want req-1", req.RequestID)
|
|
}
|
|
return RouteDecisionLogInfo{
|
|
ID: 1,
|
|
RequestID: req.RequestID,
|
|
LogicalGroupID: req.LogicalGroupID,
|
|
PublicModel: req.PublicModel,
|
|
SelectedRouteID: req.SelectedRouteID,
|
|
SelectedShadowGroupID: req.SelectedShadowGroupID,
|
|
}, nil
|
|
},
|
|
})
|
|
|
|
request := httptestRequest(t, http.MethodPost, "/api/routing/logs/decisions", map[string]any{
|
|
"request_id": "req-1",
|
|
"logical_group_id": "gpt-shared",
|
|
"public_model": "gpt-5.4",
|
|
"selected_route_id": "asxs",
|
|
"selected_shadow_group_id": "gpt-shared__asxs",
|
|
"sync": true,
|
|
}, "secret-token")
|
|
response := httptestRecorder(handler, request)
|
|
assertStatusCode(t, response, http.StatusCreated)
|
|
assertJSONContains(t, response.Body().Bytes(), "decision_log.request_id", "req-1")
|
|
}
|
|
|
|
func TestAPIListRouteDecisionLogsRejectsInvalidLimit(t *testing.T) {
|
|
handler := NewAPIHandler("secret-token", ActionSet{
|
|
ListRouteDecisionLogs: func(context.Context, ListRouteDecisionLogsRequest) ([]RouteDecisionLogInfo, error) {
|
|
t.Fatal("ListRouteDecisionLogs should not be called")
|
|
return nil, nil
|
|
},
|
|
})
|
|
|
|
request := httptestRequest(t, http.MethodGet, "/api/routing/logs/decisions?limit=zero", nil, "secret-token")
|
|
response := httptestRecorder(handler, request)
|
|
assertStatusCode(t, response, http.StatusBadRequest)
|
|
}
|
|
|
|
func TestNewActionSetRouteLoggingFlow(t *testing.T) {
|
|
dbPath := filepath.Join(t.TempDir(), "route-logging.db")
|
|
dsn := "file:" + filepath.ToSlash(dbPath) + "?_busy_timeout=5000"
|
|
actions := NewActionSet(dsn)
|
|
ctx := context.Background()
|
|
|
|
decision, err := actions.AppendRouteDecisionLog(ctx, AppendRouteDecisionLogRequest{
|
|
RequestID: "req-1",
|
|
LogicalGroupID: "gpt-shared",
|
|
PublicModel: "gpt-5.4",
|
|
StickyKey: "sticky-1",
|
|
StickyKeyType: "conversation",
|
|
StickyHit: true,
|
|
SelectedRouteID: "asxs",
|
|
SelectedShadowGroupID: "gpt-shared__asxs",
|
|
UpstreamStatus: 200,
|
|
LatencyMS: 90,
|
|
Sync: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("AppendRouteDecisionLog() error = %v", err)
|
|
}
|
|
if decision.SelectedRouteID != "asxs" {
|
|
t.Fatalf("AppendRouteDecisionLog() = %+v, want selected route asxs", decision)
|
|
}
|
|
|
|
failover, err := actions.AppendRouteFailoverEvent(ctx, AppendRouteFailoverEventRequest{
|
|
RequestID: "req-2",
|
|
LogicalGroupID: "gpt-shared",
|
|
PublicModel: "gpt-5.4",
|
|
FromRouteID: "asxs",
|
|
ToRouteID: "codex2api",
|
|
Reason: "timeout",
|
|
FailureCount: 2,
|
|
Sync: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("AppendRouteFailoverEvent() error = %v", err)
|
|
}
|
|
if failover.ToRouteID != "codex2api" {
|
|
t.Fatalf("AppendRouteFailoverEvent() = %+v, want to_route_id codex2api", failover)
|
|
}
|
|
|
|
sticky, err := actions.AppendRouteStickyAudit(ctx, AppendRouteStickyAuditRequest{
|
|
StickyKey: "sticky-1",
|
|
StickyKeyType: "conversation",
|
|
LogicalGroupID: "gpt-shared",
|
|
PublicModel: "gpt-5.4",
|
|
RouteID: "asxs",
|
|
Action: "bind",
|
|
ExpiresAt: "2026-05-28T18:00:00Z",
|
|
Sync: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("AppendRouteStickyAudit() error = %v", err)
|
|
}
|
|
if sticky.Action != "bind" {
|
|
t.Fatalf("AppendRouteStickyAudit() = %+v, want action bind", sticky)
|
|
}
|
|
|
|
decisions, err := actions.ListRouteDecisionLogs(ctx, ListRouteDecisionLogsRequest{
|
|
RequestID: "req-1",
|
|
Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ListRouteDecisionLogs() error = %v", err)
|
|
}
|
|
if len(decisions) != 1 || decisions[0].StickyKey != "sticky-1" {
|
|
t.Fatalf("ListRouteDecisionLogs() = %+v, want sticky-1", decisions)
|
|
}
|
|
|
|
failovers, err := actions.ListRouteFailoverEvents(ctx, ListRouteFailoverEventsRequest{
|
|
RequestID: "req-2",
|
|
Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ListRouteFailoverEvents() error = %v", err)
|
|
}
|
|
if len(failovers) != 1 || failovers[0].FailureCount != 2 {
|
|
t.Fatalf("ListRouteFailoverEvents() = %+v, want failure_count 2", failovers)
|
|
}
|
|
|
|
stickyAudits, err := actions.ListRouteStickyAudit(ctx, ListRouteStickyAuditRequest{
|
|
StickyKey: "sticky-1",
|
|
Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ListRouteStickyAudit() error = %v", err)
|
|
}
|
|
if len(stickyAudits) != 1 || stickyAudits[0].RouteID != "asxs" {
|
|
t.Fatalf("ListRouteStickyAudit() = %+v, want route asxs", stickyAudits)
|
|
}
|
|
}
|