Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
470 lines
19 KiB
Go
470 lines
19 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type channelMonitorV2RepoStub struct {
|
|
config serviceChannelMonitorV2ConfigAlias
|
|
users *ChannelMonitorV2List[ChannelMonitorV2UserRow]
|
|
matrix *ChannelMonitorV2Matrix
|
|
errors *ChannelMonitorV2List[ChannelMonitorV2ErrorRow]
|
|
snap *ChannelMonitorV2Snapshot
|
|
group ChannelMonitorV2GroupBy
|
|
admin bool
|
|
}
|
|
|
|
// Alias keeps composite literals readable without introducing another package.
|
|
type serviceChannelMonitorV2ConfigAlias = ChannelMonitorV2Config
|
|
|
|
func (s *channelMonitorV2RepoStub) GetConfig(context.Context) (*ChannelMonitorV2Config, error) {
|
|
cfg := ChannelMonitorV2Config(s.config)
|
|
return &cfg, nil
|
|
}
|
|
func (s *channelMonitorV2RepoStub) UpdateConfig(context.Context, ChannelMonitorV2Config, int) (*ChannelMonitorV2Config, error) {
|
|
return nil, nil
|
|
}
|
|
func (s *channelMonitorV2RepoStub) GetDimensions(context.Context, ChannelMonitorV2Filter, ChannelMonitorV2Config) (*ChannelMonitorV2Dimensions, error) {
|
|
return nil, nil
|
|
}
|
|
func (s *channelMonitorV2RepoStub) GetSnapshot(_ context.Context, _ ChannelMonitorV2Filter, _ ChannelMonitorV2Config, admin bool) (*ChannelMonitorV2Snapshot, error) {
|
|
s.admin = admin
|
|
if s.snap == nil {
|
|
return nil, nil
|
|
}
|
|
// Shallow copy so public redaction does not mutate fixture.
|
|
cfg := s.snap.Config
|
|
cfg.Platforms = append([]ChannelMonitorV2PlatformConfig(nil), s.snap.Config.Platforms...)
|
|
for i := range cfg.Platforms {
|
|
cfg.Platforms[i].Models = append([]string(nil), s.snap.Config.Platforms[i].Models...)
|
|
}
|
|
cfg.GroupIDs = append([]int64(nil), s.snap.Config.GroupIDs...)
|
|
cfg.IgnoredErrorCategories = append([]string(nil), s.snap.Config.IgnoredErrorCategories...)
|
|
out := *s.snap
|
|
out.Config = cfg
|
|
return &out, nil
|
|
}
|
|
func (s *channelMonitorV2RepoStub) GetModels(context.Context, ChannelMonitorV2Filter, ChannelMonitorV2Config, bool) (*ChannelMonitorV2List[ChannelMonitorV2ModelRow], error) {
|
|
return nil, nil
|
|
}
|
|
func (s *channelMonitorV2RepoStub) GetMatrix(_ context.Context, _ ChannelMonitorV2Filter, _ ChannelMonitorV2Config, groupBy ChannelMonitorV2GroupBy, admin bool) (*ChannelMonitorV2Matrix, error) {
|
|
s.group, s.admin = groupBy, admin
|
|
return s.matrix, nil
|
|
}
|
|
func (s *channelMonitorV2RepoStub) GetErrors(_ context.Context, _ ChannelMonitorV2Filter, _ ChannelMonitorV2Config, admin bool) (*ChannelMonitorV2List[ChannelMonitorV2ErrorRow], error) {
|
|
s.admin = admin
|
|
if s.errors != nil {
|
|
// Return a shallow copy so service redaction does not mutate the fixture.
|
|
out := &ChannelMonitorV2List[ChannelMonitorV2ErrorRow]{
|
|
Coverage: s.errors.Coverage,
|
|
Items: append([]ChannelMonitorV2ErrorRow(nil), s.errors.Items...),
|
|
}
|
|
for i := range out.Items {
|
|
if len(out.Items[i].Details) > 0 {
|
|
out.Items[i].Details = append([]ChannelMonitorV2ErrorDetail(nil), out.Items[i].Details...)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
func (s *channelMonitorV2RepoStub) GetUsers(context.Context, ChannelMonitorV2Filter, ChannelMonitorV2Config, bool) (*ChannelMonitorV2List[ChannelMonitorV2UserRow], error) {
|
|
return s.users, nil
|
|
}
|
|
func (s *channelMonitorV2RepoStub) RecomputeRange(context.Context, time.Time, time.Time) error {
|
|
return nil
|
|
}
|
|
func (s *channelMonitorV2RepoStub) GetAggregationWatermark(context.Context) (*ChannelMonitorV2AggregationWatermark, error) {
|
|
return &ChannelMonitorV2AggregationWatermark{}, nil
|
|
}
|
|
|
|
func TestChannelMonitorV2BootstrapProgress(t *testing.T) {
|
|
now := time.Date(2026, 8, 7, 12, 0, 0, 0, time.UTC)
|
|
|
|
t.Run("no data shows active zero progress", func(t *testing.T) {
|
|
b := ChannelMonitorV2BootstrapProgress(now, time.Time{}, false)
|
|
require.NotNil(t, b)
|
|
require.True(t, b.Active)
|
|
require.Equal(t, 0, b.ProgressPercent)
|
|
require.Equal(t, now.Add(-ChannelMonitorV2BootstrapProductWindow), b.TargetStart)
|
|
})
|
|
|
|
t.Run("2h seed is small percent of 30d", func(t *testing.T) {
|
|
covered := now.Add(-2 * time.Hour)
|
|
b := ChannelMonitorV2BootstrapProgress(now, covered, true)
|
|
require.NotNil(t, b)
|
|
require.True(t, b.Active)
|
|
require.Greater(t, b.ProgressPercent, 0)
|
|
require.Less(t, b.ProgressPercent, 5)
|
|
})
|
|
|
|
t.Run("30d covered hides bootstrap", func(t *testing.T) {
|
|
covered := now.Add(-ChannelMonitorV2BootstrapProductWindow)
|
|
b := ChannelMonitorV2BootstrapProgress(now, covered, true)
|
|
require.Nil(t, b)
|
|
})
|
|
|
|
t.Run("beyond 30d also hides", func(t *testing.T) {
|
|
covered := now.Add(-60 * 24 * time.Hour)
|
|
b := ChannelMonitorV2BootstrapProgress(now, covered, true)
|
|
require.Nil(t, b)
|
|
})
|
|
}
|
|
|
|
func TestChannelMonitorV2ParseFilterDefaultsAndBuckets(t *testing.T) {
|
|
svc := &ChannelMonitorV2Service{now: func() time.Time { return time.Date(2026, 8, 1, 12, 0, 0, 0, time.UTC) }}
|
|
|
|
filter, err := svc.ParseFilter("", []string{"openai", "openai", ""}, []string{"gpt-5"}, []int64{2, 1, 2, 0})
|
|
require.NoError(t, err)
|
|
require.Equal(t, "90m", filter.Range)
|
|
require.Equal(t, 5*time.Minute, filter.Bucket)
|
|
require.Equal(t, []string{"openai"}, filter.Platforms)
|
|
require.Equal(t, []int64{1, 2}, filter.GroupIDs)
|
|
require.Equal(t, 90*time.Minute, filter.End.Sub(filter.Start))
|
|
|
|
filter, err = svc.ParseFilter("30d", nil, nil, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 24*time.Hour, filter.Bucket)
|
|
_, err = svc.ParseFilter("15d", nil, nil, nil)
|
|
require.ErrorIs(t, err, ErrChannelMonitorV2InvalidRange)
|
|
}
|
|
|
|
func TestParseChannelMonitorV2GroupBy(t *testing.T) {
|
|
groupBy, err := ParseChannelMonitorV2GroupBy("")
|
|
require.NoError(t, err)
|
|
require.Equal(t, ChannelMonitorV2GroupByPlatformGroup, groupBy)
|
|
for _, value := range []ChannelMonitorV2GroupBy{ChannelMonitorV2GroupByPlatform, ChannelMonitorV2GroupByPlatformGroup, ChannelMonitorV2GroupByPlatformModel, ChannelMonitorV2GroupByPlatformGroupModel} {
|
|
parsed, parseErr := ParseChannelMonitorV2GroupBy(string(value))
|
|
require.NoError(t, parseErr)
|
|
require.Equal(t, value, parsed)
|
|
}
|
|
_, err = ParseChannelMonitorV2GroupBy("group")
|
|
require.ErrorIs(t, err, ErrChannelMonitorV2InvalidGroupBy)
|
|
}
|
|
|
|
func TestChannelMonitorV2MatrixForwardsGroupingAndAdminScope(t *testing.T) {
|
|
want := &ChannelMonitorV2Matrix{GroupBy: ChannelMonitorV2GroupByPlatformModel}
|
|
repo := &channelMonitorV2RepoStub{config: ChannelMonitorV2Config{Enabled: true}, matrix: want}
|
|
result, err := NewChannelMonitorV2Service(repo).Matrix(context.Background(), ChannelMonitorV2Filter{}, ChannelMonitorV2GroupByPlatformModel, true)
|
|
require.NoError(t, err)
|
|
require.Same(t, want, result)
|
|
require.Equal(t, ChannelMonitorV2GroupByPlatformModel, repo.group)
|
|
require.True(t, repo.admin)
|
|
|
|
_, err = NewChannelMonitorV2Service(repo).Matrix(context.Background(), ChannelMonitorV2Filter{}, "bad", false)
|
|
require.ErrorIs(t, err, ErrChannelMonitorV2InvalidGroupBy)
|
|
}
|
|
|
|
func TestChannelMonitorV2ConfigValidation(t *testing.T) {
|
|
cfg := ChannelMonitorV2Config{
|
|
Platforms: []ChannelMonitorV2PlatformConfig{
|
|
{Platform: " OpenAI ", Enabled: true, Models: []string{"gpt-5", "gpt-5", ""}},
|
|
{Platform: "anthropic", Enabled: true},
|
|
},
|
|
GroupIDs: []int64{3, 1, 3},
|
|
}
|
|
require.NoError(t, normalizeChannelMonitorV2Config(&cfg))
|
|
require.Equal(t, 300, cfg.RefreshIntervalSeconds)
|
|
require.Equal(t, "anthropic", cfg.Platforms[0].Platform)
|
|
require.Equal(t, []int64{1, 3}, cfg.GroupIDs)
|
|
|
|
cfg.RefreshIntervalSeconds = 120
|
|
require.ErrorIs(t, normalizeChannelMonitorV2Config(&cfg), ErrChannelMonitorV2InvalidConfig)
|
|
|
|
cfg.RefreshIntervalSeconds = 60
|
|
cfg.GroupIDs = []int64{0}
|
|
require.ErrorIs(t, normalizeChannelMonitorV2Config(&cfg), ErrChannelMonitorV2InvalidConfig)
|
|
}
|
|
|
|
func TestChannelMonitorV2ErrorTaxonomyPriority(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
in ChannelMonitorV2ErrorInput
|
|
want string
|
|
}{
|
|
{"cyber", ChannelMonitorV2ErrorInput{ErrorType: "cyber_policy", StatusCode: 200}, "content_policy"},
|
|
{"auth before forbidden", ChannelMonitorV2ErrorInput{StatusCode: 403, Message: "invalid API key"}, "authentication"},
|
|
{"context", ChannelMonitorV2ErrorInput{Message: "maximum prompt length exceeded"}, "context_limit"},
|
|
{"unsupported", ChannelMonitorV2ErrorInput{Message: "not supported by any configured account"}, "model_unsupported"},
|
|
{"pool", ChannelMonitorV2ErrorInput{Message: "No available accounts"}, "account_pool_unavailable"},
|
|
{"timeout", ChannelMonitorV2ErrorInput{Message: "error code: 524"}, "timeout"},
|
|
{"upstream", ChannelMonitorV2ErrorInput{ErrorOwner: "provider", StatusCode: 502, UpstreamStatusCode: 502}, "upstream_5xx"},
|
|
{"other", ChannelMonitorV2ErrorInput{StatusCode: 400, Message: "unknown"}, "other"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) { require.Equal(t, tt.want, ClassifyChannelMonitorV2Error(tt.in)) })
|
|
}
|
|
}
|
|
|
|
func TestChannelMonitorV2HealthBlendsErrorTTFTAndCache(t *testing.T) {
|
|
// error 3%/5% → 40; ttft p50 2s → 100; cache 50% → 50
|
|
// overall = (0.6*40 + 0.2*100 + 0.2*50) / 1.0 = 54 → warning
|
|
p50 := int64(2000)
|
|
p95 := int64(9000)
|
|
thresholds := ChannelMonitorV2HealthThresholds{
|
|
MinimumSample: 20,
|
|
WarningErrorRate: 0.02,
|
|
CriticalErrorRate: 0.05,
|
|
TargetTTFTMs: 2500,
|
|
WarningTTFTMs: 2501,
|
|
CriticalTTFTMs: 6000,
|
|
WarningCacheRate: 0.20,
|
|
CriticalCacheRate: 0.05,
|
|
ErrorWeight: 0.60,
|
|
TTFTWeight: 0.20,
|
|
CacheWeight: 0.20,
|
|
}
|
|
metrics := ChannelMonitorV2Metric{
|
|
RequestCount: 100,
|
|
ErrorRate: 0.03,
|
|
CacheRate: 0.50,
|
|
CacheRateDenominator: 100,
|
|
TTFT: ChannelMonitorV2Latency{SampleCount: 100, P50Ms: &p50, P95Ms: &p95},
|
|
}
|
|
health := ChannelMonitorV2HealthForWithThresholds(metrics, thresholds)
|
|
require.Equal(t, "warning", health.ErrorRate)
|
|
require.Equal(t, "healthy", health.TTFT)
|
|
require.Equal(t, "healthy", health.Cache)
|
|
require.NotNil(t, health.Score)
|
|
require.NotNil(t, health.CacheScore)
|
|
require.InDelta(t, 50.0, *health.CacheScore, 0.01)
|
|
require.InDelta(t, 54.0, *health.Score, 0.01)
|
|
require.Equal(t, "warning", health.Overall)
|
|
|
|
// Perfect signals → 100
|
|
p50OK := int64(1000)
|
|
metrics.ErrorRate = 0
|
|
metrics.CacheRate = 1
|
|
metrics.TTFT.P50Ms = &p50OK
|
|
health = ChannelMonitorV2HealthForWithThresholds(metrics, thresholds)
|
|
require.Equal(t, "healthy", health.Overall)
|
|
require.NotNil(t, health.Score)
|
|
require.InDelta(t, 100.0, *health.Score, 0.01)
|
|
|
|
// Small samples stay unknown
|
|
health = ChannelMonitorV2HealthFor(ChannelMonitorV2Metric{RequestCount: 2})
|
|
require.Equal(t, "unknown", health.Overall)
|
|
require.Nil(t, health.Score)
|
|
}
|
|
|
|
func TestChannelMonitorV2DefaultHealthThresholdsAreTolerant(t *testing.T) {
|
|
p50 := int64(2500)
|
|
health := ChannelMonitorV2HealthFor(ChannelMonitorV2Metric{
|
|
RequestCount: 100,
|
|
ErrorRate: 0.03,
|
|
CacheRate: 0,
|
|
CacheRateDenominator: 100,
|
|
TTFT: ChannelMonitorV2Latency{SampleCount: 100, P50Ms: &p50},
|
|
})
|
|
require.Equal(t, "healthy", health.ErrorRate)
|
|
require.Equal(t, "healthy", health.TTFT)
|
|
require.Equal(t, "healthy", health.Cache)
|
|
require.Equal(t, "healthy", health.Overall)
|
|
require.NotNil(t, health.CacheScore)
|
|
require.InDelta(t, 100.0, *health.CacheScore, 0.01)
|
|
}
|
|
|
|
func TestErrorRateTTFTAndCacheScoreHelpers(t *testing.T) {
|
|
require.InDelta(t, 100.0, errorRateScore(0, 0.05), 0.001)
|
|
require.InDelta(t, 0.0, errorRateScore(0.05, 0.05), 0.001)
|
|
require.InDelta(t, 40.0, errorRateScore(0.03, 0.05), 0.001)
|
|
|
|
require.InDelta(t, 100.0, ttftP50Score(2500, 2500, 6000), 0.001)
|
|
require.InDelta(t, 100.0, ttftP50Score(1000, 2500, 6000), 0.001)
|
|
require.InDelta(t, 0.0, ttftP50Score(6000, 2500, 6000), 0.001)
|
|
require.InDelta(t, 50.0, ttftP50Score(4250, 2500, 6000), 0.001)
|
|
|
|
require.InDelta(t, 0.0, cacheRateScore(0), 0.001)
|
|
require.InDelta(t, 100.0, cacheRateScore(1), 0.001)
|
|
require.InDelta(t, 50.0, cacheRateScore(0.5), 0.001)
|
|
require.Equal(t, "critical", cacheRateBand(0.02, 0.20, 0.05))
|
|
require.Equal(t, "warning", cacheRateBand(0.10, 0.20, 0.05))
|
|
require.Equal(t, "healthy", cacheRateBand(0.50, 0.20, 0.05))
|
|
}
|
|
|
|
func TestChannelMonitorV2UsersRemovesOtherUserIdentity(t *testing.T) {
|
|
selfID, otherID := int64(7), int64(9)
|
|
repo := &channelMonitorV2RepoStub{config: ChannelMonitorV2Config{Enabled: true}, users: &ChannelMonitorV2List[ChannelMonitorV2UserRow]{Items: []ChannelMonitorV2UserRow{
|
|
{UserID: &otherID, Email: "other@example.com", Username: "other"},
|
|
{UserID: &selfID, Email: "self@example.com", Username: "self"},
|
|
}}}
|
|
result, err := NewChannelMonitorV2Service(repo).Users(context.Background(), ChannelMonitorV2Filter{}, selfID, false)
|
|
require.NoError(t, err)
|
|
require.Nil(t, result.Items[0].UserID)
|
|
require.Empty(t, result.Items[0].Email)
|
|
require.Equal(t, "Other user #1", result.Items[0].DisplayLabel)
|
|
require.Equal(t, selfID, *result.Items[1].UserID)
|
|
require.True(t, result.Items[1].IsSelf)
|
|
require.Equal(t, "Me", result.Items[1].DisplayLabel)
|
|
}
|
|
|
|
func TestChannelMonitorV2UsersAppendsSelfWhenMissingFromRanking(t *testing.T) {
|
|
selfID, otherID := int64(7), int64(9)
|
|
repo := &channelMonitorV2RepoStub{config: ChannelMonitorV2Config{Enabled: true}, users: &ChannelMonitorV2List[ChannelMonitorV2UserRow]{Items: []ChannelMonitorV2UserRow{
|
|
{UserID: &otherID, Email: "other@example.com", Username: "other", Metrics: ChannelMonitorV2Metric{RequestCount: 10}},
|
|
}}}
|
|
result, err := NewChannelMonitorV2Service(repo).Users(context.Background(), ChannelMonitorV2Filter{}, selfID, false)
|
|
require.NoError(t, err)
|
|
require.Len(t, result.Items, 2)
|
|
self := result.Items[1]
|
|
require.True(t, self.IsSelf)
|
|
require.Equal(t, "Me", self.DisplayLabel)
|
|
require.Equal(t, selfID, *self.UserID)
|
|
require.Equal(t, 0, self.Rank) // unranked / no traffic in window
|
|
}
|
|
|
|
func TestChannelMonitorV2TopUsersKeepsSelfOutsideLimit(t *testing.T) {
|
|
items := make([]ChannelMonitorV2UserRow, 0, 12)
|
|
for i := 1; i <= 12; i++ {
|
|
id := int64(i)
|
|
items = append(items, ChannelMonitorV2UserRow{UserID: &id, Rank: i, DisplayLabel: fmt.Sprintf("u%d", i)})
|
|
}
|
|
// self is rank 12 (index 11)
|
|
out := channelMonitorV2TopUsersWithSelf(items, 11, 10)
|
|
require.Len(t, out, 11)
|
|
require.Equal(t, int64(12), *out[10].UserID)
|
|
require.Equal(t, 12, out[10].Rank)
|
|
}
|
|
|
|
func TestChannelMonitorV2ReadAPIsRejectDisabledConfig(t *testing.T) {
|
|
repo := &channelMonitorV2RepoStub{config: ChannelMonitorV2Config{Enabled: false}}
|
|
_, err := NewChannelMonitorV2Service(repo).Matrix(context.Background(), ChannelMonitorV2Filter{}, ChannelMonitorV2GroupByPlatform, false)
|
|
require.ErrorIs(t, err, ErrChannelMonitorDisabled)
|
|
}
|
|
|
|
func TestNormalizeChannelMonitorV2IgnoredCategories(t *testing.T) {
|
|
got := normalizeChannelMonitorV2IgnoredCategories([]string{"timeout", "TIMEOUT", "not-a-real", "", "timeout", "other"})
|
|
require.Equal(t, []string{"other", "timeout"}, got)
|
|
}
|
|
|
|
func TestApplyIgnoredErrorsAdjustsRateOnly(t *testing.T) {
|
|
// applyIgnoredErrors lives in repository package; mirror the scored-rate formula.
|
|
// scored errors = total_errors - ignored; error_rate = scored / request_count
|
|
// success_rate stays SuccessRequests / RequestCount (ignored ≠ success).
|
|
m := ChannelMonitorV2Metric{RequestCount: 100, ErrorRequests: 20, SuccessRequests: 80, ErrorRate: 0.2, SuccessRate: 0.8}
|
|
ignored := int64(5)
|
|
counted := m.ErrorRequests - ignored
|
|
errorRate := float64(counted) / float64(m.RequestCount)
|
|
successRate := float64(m.SuccessRequests) / float64(m.RequestCount)
|
|
require.InDelta(t, 0.15, errorRate, 0.0001)
|
|
require.InDelta(t, 0.80, successRate, 0.0001)
|
|
require.Equal(t, int64(20), m.ErrorRequests)
|
|
require.Equal(t, int64(100), m.RequestCount)
|
|
}
|
|
|
|
func TestErrorsForViewerStripsDetailsAndCountsForNonAdmin(t *testing.T) {
|
|
fixture := &ChannelMonitorV2List[ChannelMonitorV2ErrorRow]{
|
|
Items: []ChannelMonitorV2ErrorRow{{
|
|
Category: "timeout",
|
|
Count: 42,
|
|
Rate: 0.4,
|
|
Details: []ChannelMonitorV2ErrorDetail{{
|
|
Platform: "anthropic",
|
|
Model: "claude",
|
|
ErrorType: "upstream",
|
|
StatusCode: 504,
|
|
UpstreamStatusCode: 504,
|
|
Message: "gateway timeout sk-secret",
|
|
Count: 12,
|
|
}},
|
|
}},
|
|
}
|
|
repo := &channelMonitorV2RepoStub{config: ChannelMonitorV2Config{Enabled: true}, errors: fixture}
|
|
svc := NewChannelMonitorV2Service(repo)
|
|
|
|
userList, err := svc.ErrorsForViewer(context.Background(), ChannelMonitorV2Filter{}, false)
|
|
require.NoError(t, err)
|
|
require.False(t, repo.admin)
|
|
require.Len(t, userList.Items, 1)
|
|
require.Zero(t, userList.Items[0].Count)
|
|
require.Empty(t, userList.Items[0].Details)
|
|
require.InDelta(t, 0.4, userList.Items[0].Rate, 0.0001)
|
|
|
|
adminList, err := svc.ErrorsForViewer(context.Background(), ChannelMonitorV2Filter{}, true)
|
|
require.NoError(t, err)
|
|
require.True(t, repo.admin)
|
|
require.Equal(t, int64(42), adminList.Items[0].Count)
|
|
require.Len(t, adminList.Items[0].Details, 1)
|
|
require.Contains(t, adminList.Items[0].Details[0].Message, "gateway timeout")
|
|
}
|
|
|
|
func TestSnapshotRedactsPublicConfigPolicyFields(t *testing.T) {
|
|
updatedBy := int64(9)
|
|
repo := &channelMonitorV2RepoStub{
|
|
config: ChannelMonitorV2Config{Enabled: true},
|
|
snap: &ChannelMonitorV2Snapshot{
|
|
Config: ChannelMonitorV2Config{
|
|
Version: 3,
|
|
Enabled: true,
|
|
RefreshIntervalSeconds: 300,
|
|
Platforms: []ChannelMonitorV2PlatformConfig{
|
|
{Platform: "openai", Enabled: true, Models: []string{"gpt-5"}},
|
|
},
|
|
GroupIDs: []int64{1, 2},
|
|
IgnoredErrorCategories: []string{"timeout"},
|
|
UpdatedBy: &updatedBy,
|
|
},
|
|
Metrics: ChannelMonitorV2Metric{RequestCount: 100, ErrorRate: 0.1, SuccessRate: 0.9, RPM: 5},
|
|
},
|
|
}
|
|
svc := NewChannelMonitorV2Service(repo)
|
|
snap, err := svc.Snapshot(context.Background(), ChannelMonitorV2Filter{}, false)
|
|
require.NoError(t, err)
|
|
require.Empty(t, snap.Config.GroupIDs)
|
|
require.Empty(t, snap.Config.IgnoredErrorCategories)
|
|
require.Nil(t, snap.Config.UpdatedBy)
|
|
require.Empty(t, snap.Config.Platforms[0].Models)
|
|
require.Equal(t, 300, snap.Config.RefreshIntervalSeconds)
|
|
require.Zero(t, snap.Metrics.RequestCount)
|
|
require.InDelta(t, 0.1, snap.Metrics.ErrorRate, 0.0001)
|
|
require.Zero(t, snap.Metrics.RPM)
|
|
}
|
|
|
|
func TestRedactChannelMonitorV2MetricKeepsRates(t *testing.T) {
|
|
p50 := int64(100)
|
|
m := ChannelMonitorV2Metric{
|
|
SuccessRequests: 90, ErrorRequests: 10, RequestCount: 100,
|
|
InputTokens: 1, OutputTokens: 2, CacheCreationTokens: 3, CacheReadTokens: 4,
|
|
TokenCount: 10, RPM: 12, TPM: 34, ErrorRate: 0.1, SuccessRate: 0.9,
|
|
CacheRate: 0.5, CacheRateNumerator: 4, CacheRateDenominator: 8,
|
|
TTFT: ChannelMonitorV2Latency{SampleCount: 50, P50Ms: &p50},
|
|
}
|
|
upstream := int64(3)
|
|
m.UpstreamAffectedRequests = &upstream
|
|
redactChannelMonitorV2Metric(&m, false)
|
|
require.Zero(t, m.RequestCount)
|
|
require.Zero(t, m.ErrorRequests)
|
|
require.Zero(t, m.TokenCount)
|
|
require.Zero(t, m.CacheRateNumerator)
|
|
require.Zero(t, m.TTFT.SampleCount)
|
|
require.Nil(t, m.UpstreamAffectedRequests)
|
|
require.InDelta(t, 0.1, m.ErrorRate, 0.0001)
|
|
require.InDelta(t, 12.0, m.RPM, 0.0001)
|
|
require.InDelta(t, 34.0, m.TPM, 0.0001)
|
|
require.NotNil(t, m.TTFT.P50Ms)
|
|
require.Equal(t, int64(100), *m.TTFT.P50Ms)
|
|
|
|
redactChannelMonitorV2Metric(&m, true)
|
|
require.Zero(t, m.RPM)
|
|
require.Zero(t, m.TPM)
|
|
require.InDelta(t, 0.1, m.ErrorRate, 0.0001)
|
|
}
|
|
|
|
func TestChannelMonitorV2HealthTTFTAtTargetIsHealthy(t *testing.T) {
|
|
p50 := int64(2500)
|
|
m := ChannelMonitorV2Metric{
|
|
RequestCount: 100, ErrorRate: 0,
|
|
CacheRate: 1, CacheRateDenominator: 100,
|
|
TTFT: ChannelMonitorV2Latency{SampleCount: 100, P50Ms: &p50},
|
|
}
|
|
h := ChannelMonitorV2HealthFor(m)
|
|
require.Equal(t, "healthy", h.TTFT)
|
|
require.NotNil(t, h.TTFTScore)
|
|
require.InDelta(t, 100.0, *h.TTFTScore, 0.01)
|
|
}
|