Files
gpt-load/internal/requestlog/query_test.go
T

606 lines
19 KiB
Go

package requestlog
import (
"bytes"
"context"
"encoding/json"
"math"
"reflect"
"testing"
"time"
"gorm.io/gorm"
"gpt-load/internal/platform/redact"
"gpt-load/internal/pricing"
"gpt-load/internal/protocol"
"gpt-load/internal/storage"
"gpt-load/internal/storage/models"
"gpt-load/internal/telemetry"
"gpt-load/internal/usage"
)
func TestDecodeRequestLogRowsPreservesUsageCostAttribution(t *testing.T) {
completedAt := time.Date(2026, time.July, 27, 1, 2, 3, 4, time.FixedZone("UTC+8", 8*60*60))
rows := []models.RequestLog{{
ID: "00000000-0000-4000-8000-000000000601",
CreatedAt: completedAt,
AccessKeyID: 61,
GroupID: 17,
Protocol: string(protocol.OpenAI),
ClientModel: "client-model",
UpstreamModel: "upstream-model",
Status: string(telemetry.RequestStatusSuccess),
StatusCode: 200,
DurationMs: 25,
InputTokens: 11,
CacheReadTokens: 12,
CacheWrite5MTokens: 13,
CacheWrite1HTokens: 14,
OutputTokens: 15,
Cost: 0.1234567890123,
UsageState: string(usage.StateComplete),
CostState: string(pricing.CostStatePriced),
Attempts: models.JSON(`[]`),
}}
records, err := decodeRequestLogRows(rows)
if err != nil {
t.Fatalf("decodeRequestLogRows() error = %v", err)
}
if len(records) != 1 {
t.Fatalf("records = %#v, want one record", records)
}
got := records[0]
if got.GroupID != 17 || got.UsageState != usage.StateComplete ||
got.CostState != pricing.CostStatePriced || got.UncachedInputTokens != 11 ||
got.CacheReadTokens != 12 || got.CacheWrite5MTokens != 13 ||
got.CacheWrite1HTokens != 14 || got.OutputTokens != 15 ||
got.EstimatedCostUSD != 0.1234567890123 ||
got.CompletedAt.Format(time.RFC3339Nano) != "2026-07-26T17:02:03.000000004Z" {
t.Fatalf("decoded usage/cost record = %#v", got)
}
}
func TestDecodeRequestLogRowsIgnoresHistoricalKeyMask(t *testing.T) {
rows := []models.RequestLog{{
ID: "00000000-0000-4000-8000-000000000605",
CreatedAt: time.Date(2026, time.July, 27, 0, 0, 0, 0, time.UTC),
Protocol: string(protocol.OpenAI),
Status: string(telemetry.RequestStatusSuccess),
UsageState: string(usage.StateNotApplicable),
CostState: string(pricing.CostStateNotApplicable),
Attempts: models.JSON(
`[{"sequence":1,"group_id":7,"group_name":"Primary","key_id":11,"key_mask":"prov****safe","upstream_model":"model","status_code":200,"duration_ms":10,"failure_category":"ok","action":"terminate","will_retry":false,"error_code":"","error_summary":"","committed":true}]`,
),
}}
records, err := decodeRequestLogRows(rows)
if err != nil {
t.Fatalf("decodeRequestLogRows() error = %v", err)
}
if len(records) != 1 || len(records[0].Attempts) != 1 {
t.Fatalf("records = %#v, want one historical attempt", records)
}
encoded, err := json.Marshal(records[0].Attempts[0])
if err != nil {
t.Fatalf("marshal decoded attempt: %v", err)
}
for _, forbidden := range [][]byte{[]byte(`"key_mask"`), []byte("prov"), []byte("safe")} {
if bytes.Contains(encoded, forbidden) {
t.Fatalf("decoded attempt retains historical key material %q: %s", forbidden, encoded)
}
}
}
func TestDecodeRequestLogRowsRejectsInvalidUsageCostValues(t *testing.T) {
base := models.RequestLog{
ID: "00000000-0000-4000-8000-000000000602",
CreatedAt: time.Date(2026, time.July, 27, 0, 0, 0, 0, time.UTC),
Protocol: string(protocol.OpenAI),
Status: string(telemetry.RequestStatusSuccess),
UsageState: string(usage.StateComplete),
CostState: string(pricing.CostStatePriced),
Attempts: models.JSON(`[]`),
}
tests := []struct {
name string
mutate func(*models.RequestLog)
}{
{name: "usage state", mutate: func(row *models.RequestLog) { row.UsageState = "invalid" }},
{name: "cost state", mutate: func(row *models.RequestLog) { row.CostState = "invalid" }},
{name: "negative token", mutate: func(row *models.RequestLog) { row.InputTokens = -1 }},
{name: "negative cost", mutate: func(row *models.RequestLog) { row.Cost = -0.01 }},
{name: "infinite cost", mutate: func(row *models.RequestLog) { row.Cost = math.Inf(1) }},
{name: "not a number cost", mutate: func(row *models.RequestLog) { row.Cost = math.NaN() }},
{
name: "missing usage cannot be priced",
mutate: func(row *models.RequestLog) {
row.UsageState = string(usage.StateMissing)
row.CostState = string(pricing.CostStatePriced)
},
},
{
name: "not applicable usage cannot be unpriced",
mutate: func(row *models.RequestLog) {
row.UsageState = string(usage.StateNotApplicable)
row.CostState = string(pricing.CostStateUnpriced)
},
},
{
name: "complete usage cannot have not applicable cost",
mutate: func(row *models.RequestLog) {
row.CostState = string(pricing.CostStateNotApplicable)
},
},
{
name: "unpriced cost must be zero",
mutate: func(row *models.RequestLog) {
row.CostState = string(pricing.CostStateUnpriced)
row.Cost = 0.01
},
},
{
name: "not applicable cost must be zero",
mutate: func(row *models.RequestLog) {
row.UsageState = string(usage.StateNotApplicable)
row.CostState = string(pricing.CostStateNotApplicable)
row.Cost = 0.01
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
row := base
test.mutate(&row)
if _, err := decodeRequestLogRows([]models.RequestLog{row}); err == nil {
t.Fatal("decodeRequestLogRows() error = nil, want invalid row rejection")
}
})
}
}
func TestDecodeRequestLogRowsAcceptsApprovedUsageCostMatrix(t *testing.T) {
tests := []struct {
name string
usageState usage.State
costState pricing.CostState
cost float64
}{
{name: "complete priced", usageState: usage.StateComplete, costState: pricing.CostStatePriced, cost: 0.01},
{name: "complete unpriced", usageState: usage.StateComplete, costState: pricing.CostStateUnpriced},
{name: "partial priced", usageState: usage.StatePartial, costState: pricing.CostStatePriced, cost: 0.01},
{name: "partial unpriced", usageState: usage.StatePartial, costState: pricing.CostStateUnpriced},
{name: "missing unpriced", usageState: usage.StateMissing, costState: pricing.CostStateUnpriced},
{
name: "not applicable",
usageState: usage.StateNotApplicable,
costState: pricing.CostStateNotApplicable,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
row := models.RequestLog{
ID: "00000000-0000-4000-8000-000000000604",
CreatedAt: time.Date(2026, time.July, 27, 0, 0, 0, 0, time.UTC),
Protocol: string(protocol.OpenAI),
Status: string(telemetry.RequestStatusSuccess),
UsageState: string(test.usageState),
CostState: string(test.costState),
Cost: test.cost,
Attempts: models.JSON(`[]`),
}
records, err := decodeRequestLogRows([]models.RequestLog{row})
if err != nil {
t.Fatalf("decodeRequestLogRows() error = %v", err)
}
if len(records) != 1 || records[0].UsageState != test.usageState ||
records[0].CostState != test.costState || records[0].EstimatedCostUSD != test.cost {
t.Fatalf("records = %#v", records)
}
})
}
}
func TestServiceListUsesStableKeysetCursor(t *testing.T) {
db := openRequestLogQueryDB(t)
service := newRequestLogTestService(db)
completedAt := time.Date(2026, time.July, 24, 12, 0, 0, 123, time.UTC)
older := completedAt.Add(-time.Nanosecond)
for _, row := range []models.RequestLog{
requestLogQueryRow("00000000-0000-4000-8000-000000000100", older, 41, "older", nil),
requestLogQueryRow("00000000-0000-4000-8000-000000000101", completedAt, 41, "same-time", nil),
requestLogQueryRow("00000000-0000-4000-8000-000000000102", completedAt, 41, "same-time", nil),
requestLogQueryRow("00000000-0000-4000-8000-000000000103", completedAt, 41, "same-time", nil),
} {
createRequestLogQueryRow(t, db, row)
}
first, err := service.List(context.Background(), ListQuery{Limit: 2})
if err != nil {
t.Fatalf("first List() error = %v", err)
}
if got, want := requestIDs(first.Items), []string{
"00000000-0000-4000-8000-000000000103",
"00000000-0000-4000-8000-000000000102",
}; !reflect.DeepEqual(got, want) {
t.Fatalf("first page IDs = %v, want %v", got, want)
}
if first.NextCursor == nil ||
!first.NextCursor.CompletedAt.Equal(completedAt) ||
first.NextCursor.RequestID != "00000000-0000-4000-8000-000000000102" {
t.Fatalf("first NextCursor = %#v", first.NextCursor)
}
second, err := service.List(context.Background(), ListQuery{
Limit: 2,
Cursor: first.NextCursor,
})
if err != nil {
t.Fatalf("second List() error = %v", err)
}
if got, want := requestIDs(second.Items), []string{
"00000000-0000-4000-8000-000000000101",
"00000000-0000-4000-8000-000000000100",
}; !reflect.DeepEqual(got, want) {
t.Fatalf("second page IDs = %v, want %v", got, want)
}
if second.Items[0].CompletedAt.Equal(second.Items[1].CompletedAt) {
t.Fatalf("second page did not advance from tied timestamp: %+v", second.Items)
}
if second.NextCursor != nil {
t.Fatalf("second NextCursor = %#v, want nil", second.NextCursor)
}
}
func TestServiceListAppliesAllFiltersAndGroupJSON(t *testing.T) {
db := openRequestLogQueryDB(t)
service := newRequestLogTestService(db)
from := time.Date(2026, time.July, 24, 11, 0, 0, 0, time.UTC)
to := from.Add(time.Hour)
targetID := "00000000-0000-4000-8000-000000000201"
integerAttempts := []Attempt{
{Sequence: 1, GroupID: 12, GroupName: "retry-first", WillRetry: true},
{Sequence: 2, GroupID: 13, GroupName: "retry-second"},
}
target := requestLogQueryRow(targetID, from, 71, "client-model", integerAttempts)
target.UpstreamModel = "different-upstream-model"
target.Status = string(telemetry.RequestStatusError)
createRequestLogQueryRow(t, db, target)
atTo := requestLogQueryRow(
"00000000-0000-4000-8000-000000000202",
to,
71,
"client-model",
integerAttempts,
)
atTo.Status = string(telemetry.RequestStatusError)
createRequestLogQueryRow(t, db, atTo)
beforeFrom := requestLogQueryRow(
"00000000-0000-4000-8000-000000000203",
from.Add(-time.Nanosecond),
71,
"client-model",
integerAttempts,
)
beforeFrom.Status = string(telemetry.RequestStatusError)
createRequestLogQueryRow(t, db, beforeFrom)
stringGroup := requestLogQueryRow(
"00000000-0000-4000-8000-000000000204",
from.Add(time.Minute),
71,
"client-model",
nil,
)
stringGroup.Status = string(telemetry.RequestStatusError)
stringGroup.Attempts = models.JSON(`[{"group_id":"12"}]`)
createRequestLogQueryRow(t, db, stringGroup)
realGroup := requestLogQueryRow(
"00000000-0000-4000-8000-000000000205",
from.Add(2*time.Minute),
71,
"client-model",
nil,
)
realGroup.Status = string(telemetry.RequestStatusError)
realGroup.Attempts = models.JSON(`[{"group_id":12.0}]`)
createRequestLogQueryRow(t, db, realGroup)
upstreamOnly := requestLogQueryRow(
"00000000-0000-4000-8000-000000000206",
from.Add(3*time.Minute),
71,
"wrong-client-model",
integerAttempts,
)
upstreamOnly.UpstreamModel = "client-model"
upstreamOnly.Status = string(telemetry.RequestStatusError)
createRequestLogQueryRow(t, db, upstreamOnly)
wrongAccessKey := requestLogQueryRow(
"00000000-0000-4000-8000-000000000208",
from.Add(5*time.Minute),
72,
"client-model",
integerAttempts,
)
wrongAccessKey.Status = string(telemetry.RequestStatusError)
createRequestLogQueryRow(t, db, wrongAccessKey)
wrongStatus := requestLogQueryRow(
"00000000-0000-4000-8000-000000000209",
from.Add(6*time.Minute),
71,
"client-model",
integerAttempts,
)
createRequestLogQueryRow(t, db, wrongStatus)
zeroAttempts := requestLogQueryRow(
"00000000-0000-4000-8000-000000000207",
from.Add(4*time.Minute),
71,
"client-model",
nil,
)
zeroAttempts.Status = string(telemetry.RequestStatusError)
zeroAttempts.Attempts = nil
createRequestLogQueryRow(t, db, zeroAttempts)
groupID := uint(12)
accessKeyID := uint(71)
page, err := service.List(context.Background(), ListQuery{
From: &from,
To: &to,
GroupID: &groupID,
ClientModel: "client-model",
AccessKeyID: &accessKeyID,
Status: telemetry.RequestStatusError,
Limit: 50,
})
if err != nil {
t.Fatalf("filtered List() error = %v", err)
}
if got, want := requestIDs(page.Items), []string{targetID}; !reflect.DeepEqual(got, want) {
t.Fatalf("filtered IDs = %v, want %v", got, want)
}
requestPage, err := service.List(context.Background(), ListQuery{
RequestID: targetID,
Limit: 50,
})
if err != nil {
t.Fatalf("request ID List() error = %v", err)
}
if got, want := requestIDs(requestPage.Items), []string{targetID}; !reflect.DeepEqual(got, want) {
t.Fatalf("request ID filter = %v, want %v", got, want)
}
conflictingAccessKeyID := uint(72)
conflictingPage, err := service.List(context.Background(), ListQuery{
RequestID: targetID,
AccessKeyID: &conflictingAccessKeyID,
Limit: 50,
})
if err != nil {
t.Fatalf("conflicting request ID List() error = %v", err)
}
if len(conflictingPage.Items) != 0 {
t.Fatalf("request ID with conflicting AccessKey filter = %#v, want empty", conflictingPage.Items)
}
}
func TestServiceListGroupFilterUsesAnyAttemptWhileAttributionUsesFinalGroup(t *testing.T) {
db := openRequestLogQueryDB(t)
event := testEvent("00000000-0000-4000-8000-000000000210")
event.Attempts = []telemetry.Attempt{
{Sequence: 1, GroupID: 12, UpstreamModel: "retry-model", WillRetry: true},
{Sequence: 2, GroupID: 13, UpstreamModel: "final-model"},
}
event.UpstreamModel = "final-model"
event.Usage = telemetry.UsageObservation{
GroupID: 13,
Result: usage.Result{
State: usage.StateNotApplicable,
},
}
row := mapEvent(redact.New(), event, (*pricing.Table)(nil))
if row.GroupID != 13 {
t.Fatalf("top-level GroupID = %d, want final attribution 13", row.GroupID)
}
if err := db.Create(&row).Error; err != nil {
t.Fatalf("create attributed RequestLog: %v", err)
}
service := newRequestLogTestService(db)
for _, groupID := range []uint{12, 13} {
page, err := service.List(context.Background(), ListQuery{
GroupID: &groupID,
Limit: 50,
})
if err != nil {
t.Fatalf("List(GroupID=%d) error = %v", groupID, err)
}
if got, want := requestIDs(page.Items), []string{event.RequestID}; !reflect.DeepEqual(got, want) {
t.Fatalf("List(GroupID=%d) IDs = %v, want %v", groupID, got, want)
}
}
}
func TestServiceListBatchLoadsCurrentAccessKeyNames(t *testing.T) {
db := openRequestLogQueryDB(t)
current := models.AccessKey{
Name: "before-rename", KeyValue: "cipher-current", KeyHash: "hash-current",
KeySuffix: "0001", Status: "active", Filters: models.JSON(`{}`),
}
if err := db.Create(&current).Error; err != nil {
t.Fatalf("create current AccessKey: %v", err)
}
deleted := models.AccessKey{
Name: "deleted", KeyValue: "cipher-deleted", KeyHash: "hash-deleted",
KeySuffix: "0002", Status: "active", Filters: models.JSON(`{}`),
}
if err := db.Create(&deleted).Error; err != nil {
t.Fatalf("create deleted AccessKey: %v", err)
}
base := time.Date(2026, time.July, 24, 12, 0, 0, 0, time.UTC)
createRequestLogQueryRow(t, db, requestLogQueryRow(
"00000000-0000-4000-8000-000000000301", base, current.ID, "one", nil,
))
createRequestLogQueryRow(t, db, requestLogQueryRow(
"00000000-0000-4000-8000-000000000302", base.Add(time.Second), current.ID, "two", nil,
))
createRequestLogQueryRow(t, db, requestLogQueryRow(
"00000000-0000-4000-8000-000000000303", base.Add(2*time.Second), deleted.ID, "three", nil,
))
if err := db.Model(&current).Update("name", "after-rename").Error; err != nil {
t.Fatalf("rename AccessKey: %v", err)
}
if err := db.Delete(&deleted).Error; err != nil {
t.Fatalf("delete AccessKey: %v", err)
}
accessKeyQueries := 0
const callbackName = "test:request_log_access_key_query_count"
if err := db.Callback().Query().After("gorm:query").Register(callbackName, func(tx *gorm.DB) {
if tx.Statement.Table == "access_keys" {
accessKeyQueries++
}
}); err != nil {
t.Fatalf("register query callback: %v", err)
}
page, err := newRequestLogTestService(db).List(context.Background(), ListQuery{Limit: 50})
if err != nil {
t.Fatalf("List() error = %v", err)
}
if accessKeyQueries != 1 {
t.Fatalf("AccessKey query count = %d, want one batch query", accessKeyQueries)
}
if len(page.Items) != 3 {
t.Fatalf("items = %#v, want three", page.Items)
}
byID := make(map[string]Record, len(page.Items))
for _, item := range page.Items {
byID[item.RequestID] = item
}
for _, id := range []string{
"00000000-0000-4000-8000-000000000301",
"00000000-0000-4000-8000-000000000302",
} {
item := byID[id]
if item.AccessKey.ID != current.ID || item.AccessKey.Name == nil ||
*item.AccessKey.Name != "after-rename" || item.AccessKey.Deleted {
t.Fatalf("current AccessKey ref for %s = %#v", id, item.AccessKey)
}
}
deletedRef := byID["00000000-0000-4000-8000-000000000303"].AccessKey
if deletedRef.ID != deleted.ID || deletedRef.Name != nil || !deletedRef.Deleted {
t.Fatalf("deleted AccessKey ref = %#v", deletedRef)
}
}
func TestServiceListNormalizesNullAttemptsToEmptyArray(t *testing.T) {
db := openRequestLogQueryDB(t)
row := requestLogQueryRow(
"00000000-0000-4000-8000-000000000401",
time.Date(2026, time.July, 24, 12, 0, 0, 0, time.UTC),
91,
"zero-attempt",
nil,
)
row.Attempts = nil
createRequestLogQueryRow(t, db, row)
page, err := newRequestLogTestService(db).List(context.Background(), ListQuery{Limit: 50})
if err != nil {
t.Fatalf("List() error = %v", err)
}
if len(page.Items) != 1 || page.Items[0].Attempts == nil ||
len(page.Items[0].Attempts) != 0 {
t.Fatalf("Attempts = %#v, want non-nil empty slice", page.Items)
}
encoded, err := json.Marshal(page.Items[0])
if err != nil {
t.Fatalf("json.Marshal() error = %v", err)
}
if string(encoded) == "" || !containsJSONFragment(encoded, `"Attempts":[]`) {
t.Fatalf("encoded Record = %s, want empty attempts array", encoded)
}
}
func openRequestLogQueryDB(t *testing.T) *gorm.DB {
t.Helper()
db, err := storage.Open(":memory:")
if err != nil {
t.Fatalf("storage.Open() error = %v", err)
}
sqlDB, err := db.DB()
if err != nil {
t.Fatalf("db.DB() error = %v", err)
}
t.Cleanup(func() {
if err := sqlDB.Close(); err != nil {
t.Errorf("close request log query database: %v", err)
}
})
if err := storage.AutoMigrate(db); err != nil {
t.Fatalf("storage.AutoMigrate() error = %v", err)
}
return db
}
func requestLogQueryRow(
id string,
completedAt time.Time,
accessKeyID uint,
clientModel string,
attempts []Attempt,
) models.RequestLog {
encodedAttempts, err := json.Marshal(attempts)
if err != nil {
panic(err)
}
return models.RequestLog{
ID: id,
CreatedAt: completedAt,
AccessKeyID: accessKeyID,
Protocol: string(protocol.OpenAI),
ClientModel: clientModel,
UpstreamModel: "upstream-" + clientModel,
Status: string(telemetry.RequestStatusSuccess),
StatusCode: 200,
DurationMs: 25,
Attempts: models.JSON(encodedAttempts),
}
}
func createRequestLogQueryRow(t *testing.T, db *gorm.DB, row models.RequestLog) {
t.Helper()
if err := db.Create(&row).Error; err != nil {
t.Fatalf("create RequestLog %s: %v", row.ID, err)
}
}
func requestIDs(records []Record) []string {
ids := make([]string, 0, len(records))
for _, record := range records {
ids = append(ids, record.RequestID)
}
return ids
}
func containsJSONFragment(encoded []byte, fragment string) bool {
for index := 0; index+len(fragment) <= len(encoded); index++ {
if string(encoded[index:index+len(fragment)]) == fragment {
return true
}
}
return false
}