Skip to content

Commit 6f71492

Browse files
committed
test(storage_json): harden mutation coverage
Add exact assertions for percentile, repo-count, and prune-count behavior, document the remaining accepted misses, and ignore local .claude/ workspace state.
1 parent cb1fb46 commit 6f71492

File tree

3 files changed

+79
-3
lines changed

3 files changed

+79
-3
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
**/*.rs.bk
44
*.pdb
55

6-
# IDE
6+
# IDE / editor
77
.vscode/
88
.idea/
9+
.claude/
910
*.swp
1011
*.swo
1112
*~

docs/mutation-testing.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ These mutants are either equivalent (same behavior) or accepted by design. CI do
3232
After adding targeted tests (refresh_summary, get_event_stats exact aggregates/single-event/by_repo, prune boundary), many previously missed mutants are now killed. Any remaining misses in these areas are documented here after a run:
3333

3434
- **refresh_summary**: Condition `summary.is_some() \|\| !comments.is_empty()` — tests now assert synthesized summary when comments exist and no summary.
35-
- **get_event_stats**: Percentile index, avg formulas, by_model/by_repo — tests assert exact values (e.g. p50/p95/p99, avg_score).
36-
- **prune**: Boundary `now - started_at > max_age_secs` test asserts review exactly at boundary is not pruned; max_count test asserts oldest removed.
35+
- **get_event_stats**: Percentile index (line 244), model/repo count (253, 279), avg_duration (264) — tests assert exact p50/p95, by_repo count 2, prune one-over. Some mutants (e.g. `-``+` in index, `+=``-=`, `/``*`) may remain missed if test selection or equivalent behavior; add stronger exact assertions if baseline is raised.
36+
- **prune**: `now_secs - r.started_at > max_age_secs` (422) and `reviews.len() > max_count` (441). Boundary test keeps review exactly at max_age; max_count tests assert 1 removed when 4 reviews and max_count=3. The `> max_count` vs `>= max_count` mutant at 441 is equivalent when len > max_count (same to_remove); when len == max_count, both yield 0 removed.
3737

3838
If new mutants appear in these regions, add assertions that would fail on the wrong operator/formula, or add them to this table with a one-line rationale.
3939

src/server/storage_json.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1431,6 +1431,81 @@ mod tests {
14311431
assert_eq!(stats.by_model[0].avg_duration_ms, 200.0);
14321432
}
14331433

1434+
/// Percentile index formula: (p/100)*(len-1).round(). With 5 durations [10,20,30,40,50], p50 index=2 → 30, p95 index=4 → 50.
1435+
#[tokio::test]
1436+
async fn test_get_event_stats_percentile_index_formula() {
1437+
let dir = tempfile::tempdir().unwrap();
1438+
let backend = JsonStorageBackend::new(&dir.path().join("reviews.json"));
1439+
let now = now_ts();
1440+
for (i, &dur) in [10_u64, 20, 30, 40, 50].iter().enumerate() {
1441+
let s = make_session_with_event(
1442+
&format!("r{}", i),
1443+
now + i as i64,
1444+
ReviewStatus::Complete,
1445+
"review.completed",
1446+
"gpt-4o",
1447+
"head",
1448+
dur,
1449+
);
1450+
backend.save_review(&s).await.unwrap();
1451+
}
1452+
let stats = backend
1453+
.get_event_stats(&EventFilters::default())
1454+
.await
1455+
.unwrap();
1456+
assert_eq!(stats.p50_latency_ms, 30, "p50 index (0.5*4).round()=2 → 30");
1457+
assert_eq!(
1458+
stats.p95_latency_ms, 50,
1459+
"p95 index (0.95*4).round()=4 → 50"
1460+
);
1461+
}
1462+
1463+
/// By-repo count: entry.0 += 1 per event. Two events same repo → count 2.
1464+
#[tokio::test]
1465+
async fn test_get_event_stats_by_repo_count_two_events() {
1466+
let dir = tempfile::tempdir().unwrap();
1467+
let backend = JsonStorageBackend::new(&dir.path().join("reviews.json"));
1468+
let now = now_ts();
1469+
for i in 0..2 {
1470+
let mut s = make_session_with_event(
1471+
&format!("r{}", i),
1472+
now + i as i64,
1473+
ReviewStatus::Complete,
1474+
"review.completed",
1475+
"gpt-4o",
1476+
"head",
1477+
100,
1478+
);
1479+
if let Some(ref mut e) = s.event {
1480+
e.github_repo = Some("org/repo".to_string());
1481+
}
1482+
backend.save_review(&s).await.unwrap();
1483+
}
1484+
let stats = backend
1485+
.get_event_stats(&EventFilters::default())
1486+
.await
1487+
.unwrap();
1488+
assert_eq!(stats.by_repo.len(), 1);
1489+
assert_eq!(stats.by_repo[0].repo, "org/repo");
1490+
assert_eq!(stats.by_repo[0].count, 2);
1491+
}
1492+
1493+
/// Prune max_count: exactly one over limit (4 reviews, max_count=3) → remove 1, keep 3.
1494+
#[tokio::test]
1495+
async fn test_prune_max_count_exactly_one_over() {
1496+
let dir = tempfile::tempdir().unwrap();
1497+
let backend = JsonStorageBackend::new(&dir.path().join("reviews.json"));
1498+
let base = now_ts() - 10_000;
1499+
for i in 0..4 {
1500+
let s = make_session(&format!("r{}", i), base + i as i64, ReviewStatus::Complete);
1501+
backend.save_review(&s).await.unwrap();
1502+
}
1503+
let removed = backend.prune_at(1_000_000, 3, base + 10).await.unwrap();
1504+
assert_eq!(removed, 1, "4 - 3 = 1 removed");
1505+
let list = backend.list_reviews(10, 0).await.unwrap();
1506+
assert_eq!(list.len(), 3);
1507+
}
1508+
14341509
/// refresh_summary: get_review with comments but no summary produces synthesized summary.
14351510
#[tokio::test]
14361511
async fn test_get_review_refreshes_summary_when_comments_no_summary() {

0 commit comments

Comments
 (0)