Skip to content

Commit a60fc02

Browse files
committed
test: widen event coverage — cost.rs, daily/cost stats, OTEL payload shape
- cost: test_estimate_gpt4o_exact, case_insensitive, free_model, fallback_exact - storage_json: test_get_event_stats_daily_counts_and_total_cost (daily_counts + total_cost_estimate) - state: test_emit_wide_event_payload_has_otel_shape (@timestamp, event.name, review) Made-with: Cursor
1 parent f42cc7c commit a60fc02

File tree

3 files changed

+137
-0
lines changed

3 files changed

+137
-0
lines changed

src/server/cost.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,27 @@ mod tests {
6363
let cost = estimate_cost_usd("unknown-model-xyz", 1_000_000);
6464
assert_eq!(cost, FALLBACK_PRICE_PER_M);
6565
}
66+
67+
#[test]
68+
fn test_estimate_gpt4o_exact() {
69+
// 1M tokens × $2.5/M = $2.5
70+
assert_eq!(estimate_cost_usd("gpt-4o", 1_000_000), 2.5);
71+
}
72+
73+
#[test]
74+
fn test_estimate_case_insensitive() {
75+
assert_eq!(estimate_cost_usd("GPT-4O", 1_000_000), 2.5);
76+
assert_eq!(estimate_cost_usd("Claude-Opus-4", 2_000_000), 10.0);
77+
}
78+
79+
#[test]
80+
fn test_estimate_free_model() {
81+
assert_eq!(estimate_cost_usd("nemotron-3-nano", 1_000_000), 0.0);
82+
}
83+
84+
#[test]
85+
fn test_estimate_fallback_exact() {
86+
let cost = estimate_cost_usd("custom-xyz", 500_000);
87+
assert_eq!(cost, 0.5);
88+
}
6689
}

src/server/state.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -832,6 +832,36 @@ mod tests {
832832
assert_eq!(count_diff_files(diff), 2);
833833
}
834834

835+
#[test]
836+
fn test_emit_wide_event_payload_has_otel_shape() {
837+
let event = ReviewEventBuilder::new("r-otel", "review.completed", "head", "gpt-4o")
838+
.duration_ms(100)
839+
.build();
840+
let timestamp = event
841+
.created_at
842+
.map(|t| t.to_rfc3339())
843+
.unwrap_or_else(|| chrono::Utc::now().to_rfc3339());
844+
let payload = serde_json::json!({
845+
"@timestamp": timestamp,
846+
"event": { "name": "review.event", "kind": "event" },
847+
"review": event
848+
});
849+
let json = serde_json::to_string(&payload).unwrap();
850+
assert!(
851+
json.contains("@timestamp"),
852+
"OTEL payload must include @timestamp"
853+
);
854+
assert!(
855+
json.contains("\"name\":\"review.event\""),
856+
"OTEL payload must include event.name for filtering"
857+
);
858+
assert!(
859+
json.contains("\"review\""),
860+
"OTEL payload must include review object"
861+
);
862+
assert!(json.contains("r-otel"), "payload must contain review_id");
863+
}
864+
835865
#[test]
836866
fn test_review_event_builder_minimal() {
837867
let event = ReviewEventBuilder::new("r1", "review.completed", "head", "gpt-4o").build();

src/server/storage_json.rs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1219,6 +1219,90 @@ mod tests {
12191219
assert_eq!(stats.by_repo[0].avg_score, Some(9.0_f64));
12201220
}
12211221

1222+
#[tokio::test]
1223+
async fn test_get_event_stats_daily_counts_and_total_cost() {
1224+
let dir = tempfile::tempdir().unwrap();
1225+
let backend = JsonStorageBackend::new(&dir.path().join("reviews.json"));
1226+
1227+
let day1 = chrono::NaiveDate::from_ymd_opt(2025, 3, 1)
1228+
.unwrap()
1229+
.and_hms_opt(10, 0, 0)
1230+
.unwrap()
1231+
.and_utc();
1232+
let day2 = chrono::NaiveDate::from_ymd_opt(2025, 3, 2)
1233+
.unwrap()
1234+
.and_hms_opt(10, 0, 0)
1235+
.unwrap()
1236+
.and_utc();
1237+
1238+
let mut s1 = make_session_with_event(
1239+
"r1",
1240+
0,
1241+
ReviewStatus::Complete,
1242+
"review.completed",
1243+
"gpt-4o",
1244+
"head",
1245+
100,
1246+
);
1247+
if let Some(ref mut e) = s1.event {
1248+
e.created_at = Some(day1);
1249+
e.cost_estimate_usd = Some(1.5);
1250+
}
1251+
let mut s2 = make_session_with_event(
1252+
"r2",
1253+
1,
1254+
ReviewStatus::Failed,
1255+
"review.failed",
1256+
"gpt-4o",
1257+
"head",
1258+
200,
1259+
);
1260+
if let Some(ref mut e) = s2.event {
1261+
e.created_at = Some(day1);
1262+
e.cost_estimate_usd = Some(0.5);
1263+
}
1264+
let mut s3 = make_session_with_event(
1265+
"r3",
1266+
2,
1267+
ReviewStatus::Complete,
1268+
"review.completed",
1269+
"gpt-4o",
1270+
"staged",
1271+
300,
1272+
);
1273+
if let Some(ref mut e) = s3.event {
1274+
e.created_at = Some(day2);
1275+
e.cost_estimate_usd = Some(2.0);
1276+
}
1277+
1278+
backend.save_review(&s1).await.unwrap();
1279+
backend.save_review(&s2).await.unwrap();
1280+
backend.save_review(&s3).await.unwrap();
1281+
1282+
let stats = backend
1283+
.get_event_stats(&EventFilters::default())
1284+
.await
1285+
.unwrap();
1286+
1287+
assert_eq!(stats.daily_counts.len(), 2, "expected two distinct days");
1288+
let day1_entry = stats
1289+
.daily_counts
1290+
.iter()
1291+
.find(|d| d.date == "2025-03-01")
1292+
.unwrap();
1293+
assert_eq!(day1_entry.completed, 1);
1294+
assert_eq!(day1_entry.failed, 1);
1295+
let day2_entry = stats
1296+
.daily_counts
1297+
.iter()
1298+
.find(|d| d.date == "2025-03-02")
1299+
.unwrap();
1300+
assert_eq!(day2_entry.completed, 1);
1301+
assert_eq!(day2_entry.failed, 0);
1302+
1303+
assert_eq!(stats.total_cost_estimate, 4.0, "1.5 + 0.5 + 2.0");
1304+
}
1305+
12221306
// ---------------------------------------------------------------
12231307
// 7. is_empty (via internal state check)
12241308
// ---------------------------------------------------------------

0 commit comments

Comments
 (0)