package web import ( "context" "io" "log/slog" "net/http" "strings" "testing" "time" "gitea.mynodes.xyz/ilker/llmgw/internal/promq" "gitea.mynodes.xyz/ilker/llmgw/internal/store" ) type fakeScopeSeries struct { byGroup map[store.Grouping][]store.UsageSeries asked []store.Grouping width time.Duration since time.Time } func (f *fakeScopeSeries) UsageBucketsBy(_ context.Context, g store.Grouping, since time.Time, width time.Duration, ) ([]store.UsageSeries, error) { f.asked = append(f.asked, g) f.width, f.since = width, since return f.byGroup[g], nil } func usagePage(t *testing.T, s *fakeScopeSeries, priced Priced) *http.ServeMux { t.Helper() a := fakeAccounts{user: store.User{ID: "u1", Email: "a@example.com", IsAdmin: true}} p := NewAccountPages(a, nil, nil, "v0", slog.New(slog.NewTextHandler(io.Discard, nil))) mux := http.NewServeMux() p.RegisterAdminUsage(mux, s, priced) return mux } // hourly builds a group whose traffic rises over six slices, so a chart drawn // from it has a shape rather than a dot. func hourly(g store.Grouping, id, name string, scale int64) store.UsageSeries { u := store.UsageSeries{Grouping: g, ID: id, Name: name} base := time.Now().Add(-6 * time.Hour).Truncate(time.Hour) for i := int64(1); i <= 6; i++ { b := store.Bucket{ Start: base.Add(time.Duration(i) * time.Hour), Requests: i * scale, Prompt: i * scale * 100, Completion: i * scale * 20, CostMicros: i * scale * 1000, } u.Buckets = append(u.Buckets, b) u.Requests += b.Requests u.Prompt += b.Prompt u.Completion += b.Completion u.CostMicros += b.CostMicros } return u } func TestTheUsagePageDrawsAndTabulatesEachGroup(t *testing.T) { s := &fakeScopeSeries{byGroup: map[store.Grouping][]store.UsageSeries{ store.GroupByTeam: { hourly(store.GroupByTeam, "t1", "platform", 3), hourly(store.GroupByTeam, "t2", "research", 1), }, }} body := mirrorGet(t, usagePage(t, s, pricedYes{}), "/admin/usage").Body.String() for _, want := range []string{"platform", "research"} { if !strings.Contains(body, want) { t.Errorf("%q is missing from the page", want) } } // A chart is an SVG with lines in it, not a frame saying there is nothing. if strings.Count(body, " s2.width) { t.Errorf("a 30-day window bucketed at %s, a 24-hour one at %s", wide, s2.width) } if s2.width > time.Hour { t.Errorf("a day in %s slices is too coarse to read", s2.width) } } func TestWithNoPricesTheSpendChartIsReplacedBySayingWhy(t *testing.T) { s := &fakeScopeSeries{byGroup: map[store.Grouping][]store.UsageSeries{ store.GroupByTeam: {hourly(store.GroupByTeam, "t1", "platform", 3)}, }} body := mirrorGet(t, usagePage(t, s, pricedNo{}), "/admin/usage").Body.String() // A spend chart flat along the bottom reads as a quiet month rather than as // a cluster where nothing is priced. if !strings.Contains(body, "No model has a price") { t.Errorf("the page should say why spend is empty; got %s", body) } if strings.Count(body, " 2 { t.Error("a spend chart was drawn although nothing is priced") } } func TestAPersonWithNoAccountIsNamedRatherThanBlank(t *testing.T) { // Keys issued by an operator before accounts existed carry real traffic, // and an unlabelled line in a legend is the one nobody can act on. s := &fakeScopeSeries{byGroup: map[store.Grouping][]store.UsageSeries{ store.GroupByUser: {hourly(store.GroupByUser, "", "", 2)}, }} body := mirrorGet(t, usagePage(t, s, pricedYes{}), "/admin/usage?by=user").Body.String() if !strings.Contains(body, "keys with no owner") { t.Errorf("unowned traffic was left unlabelled; got %s", body) } } func TestTheUsagePageCountsTheGPUTimeEachOwnerHeld(t *testing.T) { a := fakeAccounts{user: store.User{ID: "u1", Email: "a@example.com", IsAdmin: true}} p := NewAccountPages(a, nil, nil, "v0", slog.New(slog.NewTextHandler(io.Discard, nil))) // Over a day in 20-minute steps: team:research held 8 cards throughout, // nobody's model 2 for the last three steps. res, none := promq.Series{Name: "team:research"}, promq.Series{Name: ""} for i := range 72 { res.Points = append(res.Points, promq.Point{Value: 8}) if i >= 69 { none.Points = append(none.Points, promq.Point{Value: 2}) } } h := &fakeHistory{byExpr: map[string][]promq.Series{`sum by (owner) (llmgw_gpu_assigned)`: {res, none}}} p.SetHistory(h) mux := http.NewServeMux() p.RegisterAdminUsage(mux, &fakeScopeSeries{}, nil) body := mirrorGet(t, mux, "/admin/usage?window=24h").Body.String() for _, want := range []string{"GPU time, by owner", "team:research192.0", "no owner2.0"} { if !strings.Contains(body, want) { t.Errorf("the page lacks %q", want) } } // Without a metrics store, nothing is said about GPU time. if body := mirrorGet(t, usagePage(t, &fakeScopeSeries{}, nil), "/admin/usage").Body.String(); strings.Contains(body, "GPU time") { t.Error("GPU time shown with no metrics store to read it from") } }