Files
goclaw/internal/gateway/methods/usage_test.go
T
Duc Nguyen 747b58d24c fix(usage): repair cost analytics and display precision (#1330)
fix(usage): repair cost analytics and display precision

- Automatic OpenRouter pricing sync with cost backfill for traces/snapshots/events
- Live usage data merging for current-hour dashboard accuracy
- 2-decimal API cost formatting across usage/overview pages
- Comprehensive test coverage (PG, SQLite, HTTP, UI)

Merged by github-maintain automation.
2026-07-03 06:24:21 +07:00

107 lines
3.0 KiB
Go

package methods
import (
"context"
"encoding/json"
"reflect"
"testing"
"time"
"github.com/google/uuid"
"github.com/nextlevelbuilder/goclaw/internal/gateway"
"github.com/nextlevelbuilder/goclaw/internal/permissions"
"github.com/nextlevelbuilder/goclaw/internal/store"
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
)
type usageSessionStore struct {
store.SessionStore
sessions []store.SessionInfoRich
opts []store.SessionListOpts
}
func (s *usageSessionStore) ListPagedRich(_ context.Context, opts store.SessionListOpts) store.SessionListRichResult {
s.opts = append(s.opts, opts)
return store.SessionListRichResult{Sessions: s.sessions, Total: len(s.sessions)}
}
type usageTracingStore struct {
store.TracingStore
costs map[string]float64
keys []string
}
func (s *usageTracingStore) GetSessionCosts(_ context.Context, sessionKeys []string) (map[string]float64, error) {
s.keys = append(s.keys[:0], sessionKeys...)
return s.costs, nil
}
func TestUsageGetIncludesTraceCost(t *testing.T) {
now := time.Date(2026, 7, 2, 12, 0, 0, 0, time.UTC)
sessions := &usageSessionStore{
sessions: []store.SessionInfoRich{
{
SessionInfo: store.SessionInfo{Key: "agent:cppai-pm:direct:user-a", Updated: now},
Model: "qwen3.7-plus",
Provider: "bailian",
InputTokens: 1_200,
OutputTokens: 300,
},
{
SessionInfo: store.SessionInfo{Key: "agent:itsddvn:direct:user-b", Updated: now.Add(-time.Minute)},
Model: "gpt-5.5",
Provider: "openai",
InputTokens: 500,
OutputTokens: 100,
},
},
}
tracing := &usageTracingStore{costs: map[string]float64{
"agent:cppai-pm:direct:user-a": 0.0123,
"agent:itsddvn:direct:user-b": 0.0045,
}}
methods := NewUsageMethods(sessions, tracing)
tenantID := uuid.Must(uuid.NewV7())
client, responses := gateway.NewCapturingTestClient(permissions.RoleViewer, tenantID, "caller", 1)
ctx := store.WithTenantID(context.Background(), tenantID)
methods.handleGet(ctx, client, sessionReqFrame(t, protocol.MethodUsageGet, map[string]any{"limit": 1}))
resp := readUsageResponse(t, responses)
if resp.Error != nil {
t.Fatalf("unexpected error: %+v", resp.Error)
}
payload, ok := resp.Payload.(map[string]any)
if !ok {
t.Fatalf("payload type = %T", resp.Payload)
}
records, ok := payload["records"].([]any)
if !ok {
t.Fatalf("records type = %T", payload["records"])
}
if len(records) != 1 {
t.Fatalf("len(records) = %d, want 1", len(records))
}
record, ok := records[0].(map[string]any)
if !ok {
t.Fatalf("record type = %T", records[0])
}
if got, want := record["cost"], 0.0123; got != want {
t.Fatalf("cost = %#v, want %#v", got, want)
}
if want := []string{"agent:cppai-pm:direct:user-a"}; !reflect.DeepEqual(tracing.keys, want) {
t.Fatalf("cost keys = %#v, want %#v", tracing.keys, want)
}
}
func readUsageResponse(t *testing.T, ch <-chan []byte) protocol.ResponseFrame {
t.Helper()
raw := <-ch
var resp protocol.ResponseFrame
if err := json.Unmarshal(raw, &resp); err != nil {
t.Fatalf("decode response: %v", err)
}
return resp
}