#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::*;
use serial_test::serial;
use tempfile::TempDir;
fn temp_db() -> (TempDir, Connection) {
let dir = TempDir::new().expect("tempdir");
let path = dir.path().join("tracking.db");
let conn = open_db(&path).expect("open_db");
(dir, conn)
}
#[test]
#[serial]
fn db_path_env_override() {
let dir = TempDir::new().expect("tempdir");
let custom = dir.path().join("custom.db");
let _guard = crate::paths::DbPathGuard::set(custom.clone());
let result = db_path();
assert_eq!(result, Some(custom));
}
#[test]
#[serial]
fn db_path_tokf_home_override() {
let dir = TempDir::new().expect("tempdir");
let _guard = crate::paths::HomeGuard::set(dir.path());
let result = db_path();
assert_eq!(result, Some(dir.path().join("tracking.db")));
}
#[test]
#[serial]
fn db_path_tokf_db_path_wins_over_tokf_home() {
let dir = TempDir::new().expect("tempdir");
let custom = dir.path().join("custom.db");
let home_dir = TempDir::new().expect("tempdir");
let _db_guard = crate::paths::DbPathGuard::set(custom.clone());
let _home_guard = crate::paths::HomeGuard::set(home_dir.path());
let result = db_path();
assert_eq!(result, Some(custom));
}
#[test]
fn open_db_creates_dir_and_schema() {
let dir = TempDir::new().expect("tempdir");
let path = dir.path().join("sub").join("tracking.db");
let conn = open_db(&path).expect("open_db");
let count: i64 = conn
.query_row("SELECT COUNT(*) FROM events", [], |r| r.get(0))
.expect("query");
assert_eq!(count, 0);
}
#[test]
fn open_db_idempotent() {
let dir = TempDir::new().expect("tempdir");
let path = dir.path().join("tracking.db");
open_db(&path).expect("first open");
open_db(&path).expect("second open — must not error");
}
#[test]
fn record_event_inserts_row() {
let (_dir, conn) = temp_db();
let ev = build_event("echo hi", None, None, 100, 50, 100, 5, 0, false);
record_event(&conn, &ev).expect("record");
let count: i64 = conn
.query_row("SELECT COUNT(*) FROM events", [], |r| r.get(0))
.expect("count");
assert_eq!(count, 1);
}
#[test]
fn record_event_all_fields_persisted() {
let (_dir, conn) = temp_db();
let ev = build_event(
"git status",
Some("git status"),
None,
400,
200,
400,
10,
0,
false,
);
record_event(&conn, &ev).expect("record");
let (cmd, fname, ib, ob, it, ot, ft, ec): (
String,
Option<String>,
i64,
i64,
i64,
i64,
i64,
i32,
) = conn
.query_row(
"SELECT command, filter_name, input_bytes, output_bytes,
input_tokens_est, output_tokens_est,
filter_time_ms, exit_code
FROM events",
[],
|r| {
Ok((
r.get(0)?,
r.get(1)?,
r.get(2)?,
r.get(3)?,
r.get(4)?,
r.get(5)?,
r.get(6)?,
r.get(7)?,
))
},
)
.expect("select");
assert_eq!(cmd, "git status");
assert_eq!(fname.as_deref(), Some("git status"));
assert_eq!(ib, 400);
assert_eq!(ob, 200);
assert_eq!(it, 100); assert_eq!(ot, 50); assert_eq!(ft, 10);
assert_eq!(ec, 0);
}
#[test]
fn record_event_exit_code_and_filter_time_persisted() {
let (_dir, conn) = temp_db();
let ev = build_event(
"cargo test",
Some("cargo test"),
None,
800,
200,
800,
99,
42,
false,
);
record_event(&conn, &ev).expect("record");
let (ft, ec): (i64, i32) = conn
.query_row("SELECT filter_time_ms, exit_code FROM events", [], |r| {
Ok((r.get(0)?, r.get(1)?))
})
.expect("select");
assert_eq!(ft, 99, "filter_time_ms not persisted correctly");
assert_eq!(ec, 42, "exit_code not persisted correctly");
}
#[test]
fn record_event_timestamp_iso8601() {
let (_dir, conn) = temp_db();
let ev = build_event("cmd", None, None, 0, 0, 0, 0, 0, false);
record_event(&conn, &ev).expect("record");
let ts: String = conn
.query_row("SELECT timestamp FROM events", [], |r| r.get(0))
.expect("ts");
assert!(ts.len() >= 10, "timestamp too short: {ts}");
let date_part = &ts[..10];
assert!(
date_part.chars().nth(4) == Some('-') && date_part.chars().nth(7) == Some('-'),
"bad ISO date: {ts}"
);
}
#[test]
fn build_event_token_estimation() {
let ev = build_event("x", None, None, 400, 0, 400, 0, 0, false);
assert_eq!(ev.input_tokens_est, 100);
let ev2 = build_event("x", None, None, 399, 0, 399, 0, 0, false);
assert_eq!(ev2.input_tokens_est, 99);
}
#[test]
fn build_event_passthrough_filter_name_none() {
let ev = build_event("echo hi", None, None, 10, 10, 10, 0, 0, false);
assert!(ev.filter_name.is_none());
}
#[test]
fn query_summary_empty_db() {
let (_dir, conn) = temp_db();
let s = query_summary(&conn).expect("summary");
assert_eq!(s.total_commands, 0);
assert_eq!(s.total_input_tokens, 0);
assert_eq!(s.total_output_tokens, 0);
assert_eq!(s.tokens_saved, 0);
assert!(s.savings_pct.abs() < f64::EPSILON);
assert_eq!(s.pipe_override_count, 0);
}
#[test]
fn query_summary_with_events() {
let (_dir, conn) = temp_db();
let ev = build_event("cmd", Some("f"), None, 400, 100, 400, 5, 0, false);
record_event(&conn, &ev).expect("record");
let s = query_summary(&conn).expect("summary");
assert_eq!(s.total_commands, 1);
assert_eq!(s.total_input_tokens, 100);
assert_eq!(s.total_output_tokens, 25);
assert_eq!(s.tokens_saved, 75);
assert!((s.savings_pct - 75.0).abs() < 0.01);
}
#[test]
fn query_summary_zero_input_no_divide_by_zero() {
let (_dir, conn) = temp_db();
let ev = build_event("cmd", None, None, 0, 0, 0, 0, 0, false);
record_event(&conn, &ev).expect("record");
let s = query_summary(&conn).expect("summary");
assert!(s.savings_pct.abs() < f64::EPSILON); }
#[test]
fn query_summary_accumulates_multiple_events() {
let (_dir, conn) = temp_db();
let events = [
build_event("cmd1", Some("f1"), None, 400, 100, 400, 5, 0, false),
build_event("cmd2", Some("f2"), None, 800, 400, 800, 10, 1, false),
build_event("cmd3", None, None, 1200, 0, 1200, 0, 0, false),
];
for ev in &events {
record_event(&conn, ev).expect("record");
}
let s = query_summary(&conn).expect("summary");
assert_eq!(s.total_commands, 3);
assert_eq!(s.total_input_tokens, 600); assert_eq!(s.total_output_tokens, 125); assert_eq!(s.tokens_saved, 475); assert!((s.savings_pct - 79.166_666).abs() < 0.01);
}
#[test]
fn query_by_filter_groups_correctly() {
let (_dir, conn) = temp_db();
for fname in &["alpha", "beta", "gamma"] {
let ev = build_event("cmd", Some(fname), None, 400, 100, 400, 0, 0, false);
record_event(&conn, &ev).expect("record");
}
let rows = query_by_filter(&conn).expect("query");
assert_eq!(rows.len(), 3);
assert!(rows.iter().all(|r| r.commands == 1));
}
#[test]
fn query_by_filter_null_shown_as_passthrough() {
let (_dir, conn) = temp_db();
let ev = build_event("echo hi", None, None, 200, 200, 200, 0, 0, false);
record_event(&conn, &ev).expect("record");
let rows = query_by_filter(&conn).expect("query");
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].filter_name, "passthrough");
}
#[test]
fn query_by_filter_mixed_null_and_named() {
let (_dir, conn) = temp_db();
record_event(
&conn,
&build_event(
"git status",
Some("git status"),
None,
400,
100,
400,
5,
0,
false,
),
)
.expect("record");
record_event(
&conn,
&build_event("echo hi", None, None, 200, 200, 200, 0, 0, false),
)
.expect("record");
let rows = query_by_filter(&conn).expect("query");
assert_eq!(rows.len(), 2);
let names: Vec<&str> = rows.iter().map(|r| r.filter_name.as_str()).collect();
assert!(names.contains(&"git status"), "rows: {names:?}");
assert!(names.contains(&"passthrough"), "rows: {names:?}");
}
#[test]
fn query_by_filter_ordered_by_savings_desc() {
let (_dir, conn) = temp_db();
record_event(
&conn,
&build_event("cmd", Some("small"), None, 100, 80, 100, 0, 0, false),
)
.expect("record");
record_event(
&conn,
&build_event("cmd", Some("big"), None, 400, 0, 400, 0, 0, false),
)
.expect("record");
let rows = query_by_filter(&conn).expect("query");
assert_eq!(rows.len(), 2);
assert_eq!(
rows[0].filter_name, "big",
"highest savings should be first"
);
assert_eq!(rows[1].filter_name, "small");
}
#[test]
fn query_daily_groups_by_date() {
let (_dir, conn) = temp_db();
for _ in 0..2 {
let ev = build_event("cmd", None, None, 400, 100, 400, 0, 0, false);
record_event(&conn, &ev).expect("record");
}
let rows = query_daily(&conn).expect("query");
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].commands, 2);
}