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) } }