Files
wehub-resource-sync f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

797 lines
22 KiB
Go

package service_test
import (
"context"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.kenn.io/agentsview/internal/db"
"go.kenn.io/agentsview/internal/dbtest"
"go.kenn.io/agentsview/internal/service"
)
func seedPairwiseUsageFixture(t *testing.T, d *db.DB) {
t.Helper()
type usageSeed struct {
id string
project string
started string
model string
input int
output int
create int
read int
}
seeds := []usageSeed{
{
id: "usage-alpha-sonnet",
project: "alpha",
started: "2024-06-01T09:00:00Z",
model: "claude-sonnet-4-20250514",
input: 100,
output: 50,
create: 10,
read: 20,
},
{
id: "usage-beta-gpt",
project: "beta",
started: "2024-06-01T10:00:00Z",
model: "gpt-4o",
input: 30,
output: 15,
create: 0,
read: 5,
},
{
id: "usage-beta-sonnet",
project: "beta",
started: "2024-06-01T11:00:00Z",
model: "claude-sonnet-4-20250514",
input: 70,
output: 35,
create: 0,
read: 0,
},
}
for _, seed := range seeds {
started := seed.started
dbtest.SeedSessionWithMessages(
t,
d,
seed.id,
seed.project,
[]db.Message{
dbtest.UserMsg(seed.id, 0, "compare usage"),
assistantUsageMsg(seed.id, 1, seed),
},
dbtest.WithMessageCounts(2, 1),
func(s *db.Session) {
s.Agent = "claude"
s.StartedAt = &started
s.EndedAt = &started
},
)
}
}
func seedCommaProjectUsageFixture(t *testing.T, d *db.DB) {
t.Helper()
started := "2024-06-01T09:00:00Z"
for _, seed := range []struct {
id string
project string
input int
}{
{id: "usage-comma", project: "team,core", input: 100},
{id: "usage-other", project: "other", input: 25},
} {
msg := dbtest.AsstMsg(seed.id, 1, "done")
msg.Timestamp = started
msg.Model = "test-model"
msg.TokenUsage = fmt.Appendf(nil,
`{"input_tokens":%d}`, seed.input)
dbtest.SeedSessionWithMessages(
t,
d,
seed.id,
seed.project,
[]db.Message{
dbtest.UserMsg(seed.id, 0, "compare usage"),
msg,
},
dbtest.WithMessageCounts(2, 1),
func(s *db.Session) {
s.Agent = "claude"
s.StartedAt = &started
s.EndedAt = &started
},
)
}
}
func assistantUsageMsg(
sessionID string, ordinal int, seed struct {
id string
project string
started string
model string
input int
output int
create int
read int
},
) db.Message {
msg := dbtest.AsstMsg(sessionID, ordinal, "done")
msg.Timestamp = seed.started
msg.Model = seed.model
msg.TokenUsage = fmt.Appendf(nil,
`{"input_tokens":%d,"output_tokens":%d,"cache_creation_input_tokens":%d,"cache_read_input_tokens":%d}`,
seed.input,
seed.output,
seed.create,
seed.read,
)
return msg
}
func TestBuildUsageFilter_ValidMapping(t *testing.T) {
t.Parallel()
f, err := service.BuildUsageFilter(service.UsageRequest{
From: "2024-06-01",
To: "2024-06-15",
Project: "proj",
Agent: "claude",
// IncludeOneShot/IncludeAutomated default false -> exclude true.
})
require.NoError(t, err)
assert.Equal(t, "2024-06-01", f.From)
assert.Equal(t, "2024-06-15", f.To)
assert.Equal(t, "proj", f.Project)
assert.Equal(t, "UTC", f.Timezone, "empty timezone defaults to UTC")
assert.True(t, f.ExcludeOneShot, "IncludeOneShot=false -> ExcludeOneShot=true")
assert.True(t, f.ExcludeAutomated, "IncludeAutomated=false -> ExcludeAutomated=true")
assert.True(t, f.Breakdowns, "summary needs per-day breakdowns")
}
func TestBuildUsageFilter_IncludeFlagsInvert(t *testing.T) {
t.Parallel()
f, err := service.BuildUsageFilter(service.UsageRequest{
From: "2024-06-01",
To: "2024-06-02",
IncludeOneShot: true,
IncludeAutomated: true,
})
require.NoError(t, err)
assert.False(t, f.ExcludeOneShot)
assert.False(t, f.ExcludeAutomated)
}
func TestBuildUsageFilter_Validation(t *testing.T) {
t.Parallel()
cases := []struct {
name string
req service.UsageRequest
}{
{"bad timezone", service.UsageRequest{Timezone: "Fake/Zone"}},
{"bad from date", service.UsageRequest{From: "yesterday", To: "2024-06-02"}},
{"from after to", service.UsageRequest{From: "2024-07-01", To: "2024-06-01"}},
{"bad active_since", service.UsageRequest{
From: "2024-06-01", To: "2024-06-02", ActiveSince: "not-a-ts",
}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
_, err := service.BuildUsageFilter(tc.req)
require.Error(t, err)
var ue *service.UsageInputError
assert.True(t, errors.As(err, &ue),
"want UsageInputError, got %T", err)
})
}
}
func TestDirectBackend_UsageSummary_InvalidInput(t *testing.T) {
t.Parallel()
d := dbtest.OpenTestDB(t)
be := service.NewDirectBackend(d, nil)
_, err := be.UsageSummary(context.Background(), service.UsageRequest{
Timezone: "Fake/Zone",
})
require.Error(t, err)
var ue *service.UsageInputError
assert.True(t, errors.As(err, &ue), "want UsageInputError, got %T", err)
}
func TestDirectBackend_UsageSummary_UnknownProjectKeyHasStableCode(t *testing.T) {
t.Parallel()
d := dbtest.OpenTestDB(t)
be := service.NewDirectBackend(d, nil)
_, err := be.UsageSummary(context.Background(), service.UsageRequest{
ExcludeProjectKey: "pl1:sha256:stale",
})
require.Error(t, err)
var inputErr *service.UsageInputError
require.ErrorAs(t, err, &inputErr)
assert.Equal(t, service.UsageErrorCodeUnknownProjectKey, inputErr.Code)
}
func TestDirectBackend_UsageSummary_EmptyRange(t *testing.T) {
t.Parallel()
d := dbtest.OpenTestDB(t)
be := service.NewDirectBackend(d, nil)
res, err := be.UsageSummary(context.Background(), service.UsageRequest{
From: "2024-06-01", To: "2024-06-03",
})
require.NoError(t, err)
require.NotNil(t, res)
assert.Equal(t, "2024-06-01", res.From)
assert.Equal(t, "2024-06-03", res.To)
assert.NotNil(t, res.ProjectTotals, "folds should be non-nil slices")
}
func TestHTTPBackend_UsageSummary_Roundtrip(t *testing.T) {
t.Parallel()
env := newHTTPBackendEnv(t)
svc := env.Backend("", false)
res, err := svc.UsageSummary(context.Background(), service.UsageRequest{
From: "2024-06-01", To: "2024-06-03",
})
require.NoError(t, err)
require.NotNil(t, res)
assert.Equal(t, "2024-06-01", res.From)
assert.Equal(t, "2024-06-03", res.To)
}
// The server defaults include_one_shot to true, so the HTTP backend must
// always send it explicitly to faithfully transmit a false value.
func TestHTTPBackend_UsageSummary_SendsExplicitIncludeOneShot(t *testing.T) {
t.Parallel()
cases := []struct {
name string
includeOneShot bool
want string
}{
{"false", false, "false"},
{"true", true, "true"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var got string
srv := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
got = r.URL.Query().Get("include_one_shot")
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"from":"x","to":"y"}`))
}))
t.Cleanup(srv.Close)
svc := service.NewHTTPBackend(srv.URL, "", false)
_, err := svc.UsageSummary(context.Background(), service.UsageRequest{
From: "2024-06-01", To: "2024-06-02",
IncludeOneShot: tc.includeOneShot,
})
require.NoError(t, err)
assert.Equal(t, tc.want, got)
})
}
}
// A read-only daemon (pg serve) returns 501 for usage; the HTTP backend
// maps that to the shared db.ErrReadOnly sentinel.
func TestHTTPBackend_UsageSummary_ReadOnly(t *testing.T) {
t.Parallel()
srv := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotImplemented)
}))
t.Cleanup(srv.Close)
svc := service.NewHTTPBackend(srv.URL, "", true)
_, err := svc.UsageSummary(context.Background(), service.UsageRequest{
From: "2024-06-01", To: "2024-06-02",
})
require.Error(t, err)
assert.True(t, errors.Is(err, db.ErrReadOnly),
"501 should map to db.ErrReadOnly, got %v", err)
}
func TestBuildUsagePairwiseFilters_Validation(t *testing.T) {
t.Parallel()
_, _, _, _, err := service.BuildUsagePairwiseFilters(
service.UsagePairwiseComparisonRequest{
UsageRequest: service.UsageRequest{From: "2024-06-01", To: "2024-06-02"},
LeftDimension: "model",
LeftValue: "claude-sonnet-4-20250514",
RightDimension: "machine",
RightValue: "host-a",
},
)
require.Error(t, err)
var ue *service.UsageInputError
assert.True(t, errors.As(err, &ue))
assert.Equal(t, "right_dimension must be model or project", ue.Msg)
}
func TestBuildUsagePairwiseFilters_PreservesNonComparedBaseFilters(t *testing.T) {
t.Parallel()
left, leftEmpty, right, rightEmpty, err := service.BuildUsagePairwiseFilters(
service.UsagePairwiseComparisonRequest{
UsageRequest: service.UsageRequest{
From: "2024-06-01",
To: "2024-06-03",
Timezone: "UTC",
Model: "model-a,model-b",
Project: "alpha,beta",
},
LeftDimension: "model",
LeftValue: "model-b",
RightDimension: "project",
RightValue: "beta",
},
)
require.NoError(t, err)
assert.False(t, leftEmpty)
assert.False(t, rightEmpty)
assert.Equal(t, "model-b", left.Model)
assert.Equal(t, "alpha,beta", left.Project)
assert.Equal(t, "model-a,model-b", right.Model)
assert.Equal(t, "beta", right.Project)
}
func TestBuildUsagePairwiseFilters_ConflictingBaseFilterMarksEmptySide(t *testing.T) {
t.Parallel()
left, leftEmpty, right, rightEmpty, err := service.BuildUsagePairwiseFilters(
service.UsagePairwiseComparisonRequest{
UsageRequest: service.UsageRequest{
From: "2024-06-01",
To: "2024-06-03",
Model: "gpt-4o",
},
LeftDimension: "model",
LeftValue: "claude-sonnet-4-20250514",
RightDimension: "model",
RightValue: "gpt-4o",
},
)
require.NoError(t, err)
assert.True(t, leftEmpty)
assert.False(t, rightEmpty)
assert.Empty(t, left.Model)
assert.Equal(t, "gpt-4o", right.Model)
}
func TestDirectBackend_UsagePairwiseComparison_ModelVsModel(t *testing.T) {
t.Parallel()
d := dbtest.OpenTestDB(t)
seedPairwiseUsageFixture(t, d)
be := service.NewDirectBackend(d, nil)
res, err := be.UsagePairwiseComparison(
context.Background(),
service.UsagePairwiseComparisonRequest{
UsageRequest: service.UsageRequest{
From: "2024-06-01",
To: "2024-06-01",
Timezone: "UTC",
IncludeOneShot: true,
},
LeftDimension: "model",
LeftValue: "claude-sonnet-4-20250514",
RightDimension: "model",
RightValue: "gpt-4o",
},
)
require.NoError(t, err)
require.NotNil(t, res)
assert.Equal(t, 2, res.Left.SessionCount)
assert.Equal(t, 285, res.Left.TotalTokens)
assert.Equal(t, 1, res.Right.SessionCount)
assert.Equal(t, 50, res.Right.TotalTokens)
assert.Equal(t, -235, res.Deltas.TotalTokensDelta)
require.NotNil(t, res.Left.CostPerSession)
require.NotNil(t, res.Right.CostPerSession)
}
func TestDirectBackend_UsagePairwiseComparison_ProjectVsModel(t *testing.T) {
t.Parallel()
d := dbtest.OpenTestDB(t)
seedPairwiseUsageFixture(t, d)
be := service.NewDirectBackend(d, nil)
summary, err := be.UsageSummary(context.Background(), service.UsageRequest{
From: "2024-06-01", To: "2024-06-01", Timezone: "UTC",
IncludeOneShot: true,
})
require.NoError(t, err)
var betaKey string
for _, project := range summary.ProjectTotals {
if project.Project == "beta" {
betaKey = project.ProjectKey
}
}
require.NotEmpty(t, betaKey)
res, err := be.UsagePairwiseComparison(
context.Background(),
service.UsagePairwiseComparisonRequest{
UsageRequest: service.UsageRequest{
From: "2024-06-01",
To: "2024-06-01",
Timezone: "UTC",
IncludeOneShot: true,
},
LeftDimension: "project",
LeftValue: betaKey,
RightDimension: "model",
RightValue: "gpt-4o",
},
)
require.NoError(t, err)
require.NotNil(t, res)
assert.Equal(t, 2, res.Left.SessionCount)
assert.Equal(t, 155, res.Left.TotalTokens)
assert.Equal(t, 1, res.Right.SessionCount)
assert.Equal(t, 50, res.Right.TotalTokens)
}
func TestDirectBackend_UsagePairwiseComparison_PreservesCommaProjectLabel(
t *testing.T,
) {
t.Parallel()
d := dbtest.OpenTestDB(t)
seedCommaProjectUsageFixture(t, d)
be := service.NewDirectBackend(d, nil)
base := service.UsageRequest{
From: "2024-06-01", To: "2024-06-01", Timezone: "UTC",
IncludeOneShot: true,
}
summary, err := be.UsageSummary(context.Background(), base)
require.NoError(t, err)
keys := make(map[string]string)
for _, project := range summary.ProjectTotals {
keys[project.Project] = project.ProjectKey
}
require.NotEmpty(t, keys["team,core"])
require.NotEmpty(t, keys["other"])
comparison, err := be.UsagePairwiseComparison(
context.Background(),
service.UsagePairwiseComparisonRequest{
UsageRequest: base,
LeftDimension: "project",
LeftValue: keys["team,core"],
RightDimension: "project",
RightValue: keys["other"],
},
)
require.NoError(t, err)
require.NotNil(t, comparison)
assert.Equal(t, 1, comparison.Left.SessionCount)
assert.Equal(t, 100, comparison.Left.TotalTokens)
assert.Equal(t, 1, comparison.Right.SessionCount)
assert.Equal(t, 25, comparison.Right.TotalTokens)
}
func TestDirectBackend_UsageSummary_ExcludesOpaqueProjectKey(t *testing.T) {
t.Parallel()
d := dbtest.OpenTestDB(t)
seedPairwiseUsageFixture(t, d)
be := service.NewDirectBackend(d, nil)
base := service.UsageRequest{
From: "2024-06-01", To: "2024-06-01", Timezone: "UTC",
IncludeOneShot: true,
}
summary, err := be.UsageSummary(context.Background(), base)
require.NoError(t, err)
var betaKey string
for _, project := range summary.ProjectTotals {
if project.Project == "beta" {
betaKey = project.ProjectKey
}
}
require.NotEmpty(t, betaKey)
base.ExcludeProjectKey = betaKey
filtered, err := be.UsageSummary(context.Background(), base)
require.NoError(t, err)
require.Len(t, filtered.ProjectTotals, 1)
assert.Equal(t, "alpha", filtered.ProjectTotals[0].Project)
}
func TestDirectBackend_UsageSummary_ExcludesSubagentOnlyProjectKey(t *testing.T) {
t.Parallel()
d := dbtest.OpenTestDB(t)
started := "2024-06-01T09:00:00Z"
msg := dbtest.AsstMsg("usage-subagent", 1, "done")
msg.Timestamp = started
msg.Model = "test-model"
msg.TokenUsage = []byte(`{"input_tokens":100}`)
dbtest.SeedSessionWithMessages(
t,
d,
"usage-subagent",
"subagent-only",
[]db.Message{
dbtest.UserMsg("usage-subagent", 0, "compare usage"),
msg,
},
dbtest.WithMessageCounts(2, 1),
func(s *db.Session) {
s.Agent = "claude"
s.RelationshipType = "subagent"
s.StartedAt = &started
s.EndedAt = &started
},
)
be := service.NewDirectBackend(d, nil)
base := service.UsageRequest{
From: "2024-06-01", To: "2024-06-01", Timezone: "UTC",
IncludeOneShot: true,
}
summary, err := be.UsageSummary(context.Background(), base)
require.NoError(t, err)
require.Len(t, summary.ProjectTotals, 1)
require.NotEmpty(t, summary.ProjectTotals[0].ProjectKey)
assert.Equal(t, "subagent-only", summary.ProjectTotals[0].Project)
base.ExcludeProjectKey = summary.ProjectTotals[0].ProjectKey
filtered, err := be.UsageSummary(context.Background(), base)
require.NoError(t, err)
assert.Empty(t, filtered.ProjectTotals)
assert.Zero(t, filtered.Totals.InputTokens)
}
func TestDirectBackend_UsageSummary_ExcludesCommaProjectByOpaqueKey(
t *testing.T,
) {
t.Parallel()
d := dbtest.OpenTestDB(t)
seedCommaProjectUsageFixture(t, d)
be := service.NewDirectBackend(d, nil)
base := service.UsageRequest{
From: "2024-06-01", To: "2024-06-01", Timezone: "UTC",
IncludeOneShot: true,
}
summary, err := be.UsageSummary(context.Background(), base)
require.NoError(t, err)
var commaKey string
for _, project := range summary.ProjectTotals {
if project.Project == "team,core" {
commaKey = project.ProjectKey
}
}
require.NotEmpty(t, commaKey)
base.ExcludeProjectKey = commaKey
filtered, err := be.UsageSummary(context.Background(), base)
require.NoError(t, err)
require.Len(t, filtered.ProjectTotals, 1)
assert.Equal(t, "other", filtered.ProjectTotals[0].Project)
assert.Equal(t, 25, filtered.ProjectTotals[0].InputTokens)
}
func TestDirectBackend_UsagePairwiseComparison_ZeroDataSide(t *testing.T) {
t.Parallel()
d := dbtest.OpenTestDB(t)
seedPairwiseUsageFixture(t, d)
be := service.NewDirectBackend(d, nil)
res, err := be.UsagePairwiseComparison(
context.Background(),
service.UsagePairwiseComparisonRequest{
UsageRequest: service.UsageRequest{
From: "2024-06-01",
To: "2024-06-01",
Timezone: "UTC",
IncludeOneShot: true,
},
LeftDimension: "project",
LeftValue: "missing-project",
RightDimension: "model",
RightValue: "gpt-4o",
},
)
require.NoError(t, err)
require.NotNil(t, res)
assert.Zero(t, res.Left.SessionCount)
assert.Zero(t, res.Left.TotalTokens)
assert.Nil(t, res.Left.CostPerSession)
assert.Nil(t, res.Deltas.TotalCostDeltaRatio)
}
func TestDirectBackend_UsagePairwiseComparison_ConflictingBaseFilterReturnsZeroData(t *testing.T) {
t.Parallel()
d := dbtest.OpenTestDB(t)
seedPairwiseUsageFixture(t, d)
be := service.NewDirectBackend(d, nil)
res, err := be.UsagePairwiseComparison(
context.Background(),
service.UsagePairwiseComparisonRequest{
UsageRequest: service.UsageRequest{
From: "2024-06-01",
To: "2024-06-01",
Timezone: "UTC",
Model: "gpt-4o",
IncludeOneShot: true,
},
LeftDimension: "model",
LeftValue: "claude-sonnet-4-20250514",
RightDimension: "model",
RightValue: "gpt-4o",
},
)
require.NoError(t, err)
require.NotNil(t, res)
assert.Zero(t, res.Left.SessionCount)
assert.Zero(t, res.Left.TotalTokens)
assert.Equal(t, 1, res.Right.SessionCount)
assert.Equal(t, 50, res.Right.TotalTokens)
}
func TestDirectBackend_UsagePairwiseComparison_DoesNotUseSentinelFilterValues(t *testing.T) {
t.Parallel()
d := dbtest.OpenTestDB(t)
seedPairwiseUsageFixture(t, d)
started := "2024-06-01T12:00:00Z"
dbtest.SeedSessionWithMessages(
t,
d,
"usage-sentinel-model",
"sentinel-project",
[]db.Message{
dbtest.UserMsg("usage-sentinel-model", 0, "compare usage"),
assistantUsageMsg("usage-sentinel-model", 1, struct {
id string
project string
started string
model string
input int
output int
create int
read int
}{
id: "usage-sentinel-model",
project: "sentinel-project",
started: started,
model: "__agentsview_no_match__",
input: 999,
output: 999,
create: 0,
read: 0,
}),
},
dbtest.WithMessageCounts(2, 1),
func(s *db.Session) {
s.Agent = "claude"
s.StartedAt = &started
s.EndedAt = &started
},
)
be := service.NewDirectBackend(d, nil)
res, err := be.UsagePairwiseComparison(
context.Background(),
service.UsagePairwiseComparisonRequest{
UsageRequest: service.UsageRequest{
From: "2024-06-01",
To: "2024-06-01",
Timezone: "UTC",
Model: "gpt-4o",
IncludeOneShot: true,
},
LeftDimension: "model",
LeftValue: "claude-sonnet-4-20250514",
RightDimension: "model",
RightValue: "gpt-4o",
},
)
require.NoError(t, err)
require.NotNil(t, res)
assert.Zero(t, res.Left.SessionCount)
assert.Zero(t, res.Left.TotalTokens)
assert.Equal(t, 1, res.Right.SessionCount)
assert.Equal(t, 50, res.Right.TotalTokens)
}
func TestHTTPBackend_UsagePairwiseComparison_Roundtrip(t *testing.T) {
t.Parallel()
env := newHTTPBackendEnv(t)
seedPairwiseUsageFixture(t, env.DB)
svc := env.Backend("", false)
res, err := svc.UsagePairwiseComparison(
context.Background(),
service.UsagePairwiseComparisonRequest{
UsageRequest: service.UsageRequest{
From: "2024-06-01",
To: "2024-06-01",
Timezone: "UTC",
IncludeOneShot: true,
},
LeftDimension: "model",
LeftValue: "claude-sonnet-4-20250514",
RightDimension: "model",
RightValue: "gpt-4o",
},
)
require.NoError(t, err)
require.NotNil(t, res)
assert.Equal(t, 2, res.Left.SessionCount)
assert.Equal(t, 1, res.Right.SessionCount)
}
func TestHTTPBackend_UsagePairwiseComparison_SerializesRequest(t *testing.T) {
t.Parallel()
var queryValues map[string]string
srv := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
queryValues = map[string]string{
"left_dimension": r.URL.Query().Get("left_dimension"),
"left_value": r.URL.Query().Get("left_value"),
"right_dimension": r.URL.Query().Get("right_dimension"),
"right_value": r.URL.Query().Get("right_value"),
"git_branch": r.URL.Query().Get("git_branch"),
"exclude_project_key": r.URL.Query().Get("exclude_project_key"),
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"left":{"sessionCount":1,"totalTokens":12},"right":{"sessionCount":2,"totalTokens":34},"deltas":{"sessionCountDelta":1,"totalTokensDelta":22}}`))
},
))
t.Cleanup(srv.Close)
svc := service.NewHTTPBackend(srv.URL, "", false)
res, err := svc.UsagePairwiseComparison(
context.Background(),
service.UsagePairwiseComparisonRequest{
UsageRequest: service.UsageRequest{
GitBranch: "alpha/main",
ExcludeProjectKey: "pl1:sha256:hidden",
},
LeftDimension: "project",
LeftValue: "alpha",
RightDimension: "model",
RightValue: "gpt-4o",
},
)
require.NoError(t, err)
require.NotNil(t, res)
assert.Equal(t, "project", queryValues["left_dimension"])
assert.Equal(t, "alpha", queryValues["left_value"])
assert.Equal(t, "model", queryValues["right_dimension"])
assert.Equal(t, "gpt-4o", queryValues["right_value"])
assert.Equal(t, "alpha/main", queryValues["git_branch"])
assert.Equal(t, "pl1:sha256:hidden", queryValues["exclude_project_key"])
assert.Equal(t, 22, res.Deltas.TotalTokensDelta)
}