Skip to content

Commit 325f86b

Browse files
authored
feat: proj cost track (#156)
1 parent 99c52e6 commit 325f86b

1 file changed

Lines changed: 136 additions & 0 deletions

File tree

internal/services/usage_test.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package services_test
2+
3+
import (
4+
"context"
5+
"os"
6+
"testing"
7+
"time"
8+
9+
"paperdebugger/internal/libs/cfg"
10+
"paperdebugger/internal/libs/db"
11+
"paperdebugger/internal/libs/logger"
12+
"paperdebugger/internal/models"
13+
"paperdebugger/internal/services"
14+
15+
"github.com/stretchr/testify/assert"
16+
"go.mongodb.org/mongo-driver/v2/bson"
17+
"go.mongodb.org/mongo-driver/v2/mongo"
18+
)
19+
20+
func setupTestUsageService(t *testing.T) (*services.UsageService, *mongo.Database) {
21+
os.Setenv("PD_MONGO_URI", "mongodb://localhost:27017")
22+
dbInstance, err := db.NewDB(cfg.GetCfg(), logger.GetLogger())
23+
if err != nil {
24+
t.Fatalf("failed to connect to test db: %v", err)
25+
}
26+
return services.NewUsageService(dbInstance, cfg.GetCfg(), logger.GetLogger()),
27+
dbInstance.Database("paperdebugger")
28+
}
29+
30+
// TestTrackUsage_FailedCompletion verifies that when a completion fails
31+
// (success=false), the cost is recorded under failed_cost (not success_cost)
32+
// across all three buckets: hourly, weekly, and lifetime.
33+
func TestTrackUsage_FailedCompletion(t *testing.T) {
34+
us, database := setupTestUsageService(t)
35+
ctx := context.Background()
36+
37+
userID := bson.NewObjectID()
38+
projectID := "test-project-" + bson.NewObjectID().Hex()
39+
cost := 0.0125
40+
41+
// Clean up after the test
42+
t.Cleanup(func() {
43+
filter := bson.M{"user_id": userID, "project_id": projectID}
44+
_, _ = database.Collection(models.HourlyUsage{}.CollectionName()).DeleteMany(ctx, filter)
45+
_, _ = database.Collection(models.WeeklyUsage{}.CollectionName()).DeleteMany(ctx, filter)
46+
_, _ = database.Collection(models.LifetimeUsage{}.CollectionName()).DeleteMany(ctx, filter)
47+
})
48+
49+
err := us.TrackUsage(ctx, userID, projectID, cost, false)
50+
assert.NoError(t, err)
51+
52+
now := time.Now()
53+
54+
// Hourly bucket: failed_cost incremented, success_cost untouched.
55+
var hourly models.HourlyUsage
56+
err = database.Collection(models.HourlyUsage{}.CollectionName()).FindOne(ctx, bson.M{
57+
"user_id": userID,
58+
"project_id": projectID,
59+
"hour_bucket": bson.NewDateTimeFromTime(models.TruncateToHour(now)),
60+
}).Decode(&hourly)
61+
assert.NoError(t, err)
62+
assert.InDelta(t, cost, hourly.FailedCost, 1e-9)
63+
assert.Equal(t, 0.0, hourly.SuccessCost)
64+
65+
// Weekly bucket.
66+
var weekly models.WeeklyUsage
67+
err = database.Collection(models.WeeklyUsage{}.CollectionName()).FindOne(ctx, bson.M{
68+
"user_id": userID,
69+
"project_id": projectID,
70+
"week_bucket": bson.NewDateTimeFromTime(models.TruncateToWeek(now)),
71+
}).Decode(&weekly)
72+
assert.NoError(t, err)
73+
assert.InDelta(t, cost, weekly.FailedCost, 1e-9)
74+
assert.Equal(t, 0.0, weekly.SuccessCost)
75+
76+
// Lifetime bucket.
77+
var lifetime models.LifetimeUsage
78+
err = database.Collection(models.LifetimeUsage{}.CollectionName()).FindOne(ctx, bson.M{
79+
"user_id": userID,
80+
"project_id": projectID,
81+
}).Decode(&lifetime)
82+
assert.NoError(t, err)
83+
assert.InDelta(t, cost, lifetime.FailedCost, 1e-9)
84+
assert.Equal(t, 0.0, lifetime.SuccessCost)
85+
}
86+
87+
// TestTrackUsage_FailedThenSuccess verifies that failed and successful
88+
// completions accumulate into separate fields on the same bucket document.
89+
func TestTrackUsage_FailedThenSuccess(t *testing.T) {
90+
us, database := setupTestUsageService(t)
91+
ctx := context.Background()
92+
93+
userID := bson.NewObjectID()
94+
projectID := "test-project-" + bson.NewObjectID().Hex()
95+
failedCost := 0.02
96+
successCost := 0.05
97+
98+
t.Cleanup(func() {
99+
filter := bson.M{"user_id": userID, "project_id": projectID}
100+
_, _ = database.Collection(models.HourlyUsage{}.CollectionName()).DeleteMany(ctx, filter)
101+
_, _ = database.Collection(models.WeeklyUsage{}.CollectionName()).DeleteMany(ctx, filter)
102+
_, _ = database.Collection(models.LifetimeUsage{}.CollectionName()).DeleteMany(ctx, filter)
103+
})
104+
105+
assert.NoError(t, us.TrackUsage(ctx, userID, projectID, failedCost, false))
106+
assert.NoError(t, us.TrackUsage(ctx, userID, projectID, successCost, true))
107+
108+
var lifetime models.LifetimeUsage
109+
err := database.Collection(models.LifetimeUsage{}.CollectionName()).FindOne(ctx, bson.M{
110+
"user_id": userID,
111+
"project_id": projectID,
112+
}).Decode(&lifetime)
113+
assert.NoError(t, err)
114+
assert.InDelta(t, failedCost, lifetime.FailedCost, 1e-9)
115+
assert.InDelta(t, successCost, lifetime.SuccessCost, 1e-9)
116+
}
117+
118+
// TestTrackUsage_ZeroCostNoOp verifies that a zero-cost failed completion
119+
// (e.g., the provider never returned a usage chunk) writes nothing.
120+
func TestTrackUsage_ZeroCostNoOp(t *testing.T) {
121+
us, database := setupTestUsageService(t)
122+
ctx := context.Background()
123+
124+
userID := bson.NewObjectID()
125+
projectID := "test-project-" + bson.NewObjectID().Hex()
126+
127+
err := us.TrackUsage(ctx, userID, projectID, 0, false)
128+
assert.NoError(t, err)
129+
130+
count, err := database.Collection(models.LifetimeUsage{}.CollectionName()).CountDocuments(ctx, bson.M{
131+
"user_id": userID,
132+
"project_id": projectID,
133+
})
134+
assert.NoError(t, err)
135+
assert.Equal(t, int64(0), count)
136+
}

0 commit comments

Comments
 (0)