use crate::common::Drip;
use std::fs;
use std::io::Write;
use std::process::Stdio;
fn seed(drip: &Drip) -> std::path::PathBuf {
let dir = tempfile::tempdir().unwrap();
let f = dir.path().join("seed.py");
let body: String = (0..120).map(|i| format!("line {i}\n")).collect();
fs::write(&f, &body).unwrap();
drip.read_stdout(&f);
drip.read_stdout(&f);
let path = f.clone();
std::mem::forget(dir);
path
}
fn meter_total_reads(drip: &Drip) -> i64 {
let o = drip.cmd().arg("meter").arg("--json").output().unwrap();
let v: serde_json::Value = serde_json::from_slice(&o.stdout).unwrap();
v["total_reads"].as_i64().unwrap()
}
fn db_row_count(drip: &Drip, table: &str) -> i64 {
let db = drip.data_dir.path().join("sessions.db");
let conn = rusqlite::Connection::open(&db).unwrap();
conn.query_row(&format!("SELECT COUNT(*) FROM {table}"), [], |r| r.get(0))
.unwrap_or(0)
}
#[test]
fn reset_all_force_wipes_every_table_and_reports_counts() {
let drip = Drip::new();
seed(&drip);
assert!(meter_total_reads(&drip) > 0, "fixture seeded");
assert!(db_row_count(&drip, "sessions") >= 1);
assert!(db_row_count(&drip, "lifetime_stats") >= 1);
assert!(db_row_count(&drip, "file_registry") >= 1);
let o = drip
.cmd()
.args(["reset", "--all", "--force"])
.output()
.unwrap();
assert!(
o.status.success(),
"reset --all --force exited non-zero: stderr={}",
String::from_utf8_lossy(&o.stderr)
);
let out = String::from_utf8_lossy(&o.stdout);
assert!(
out.contains("All DRIP data cleared"),
"expected wipe-confirmation header: {out}"
);
assert!(
out.contains("sessions") && out.contains("reads") && out.contains("registry"),
"report must enumerate buckets that got cleared: {out}"
);
for tbl in [
"sessions",
"reads",
"file_registry",
"lifetime_stats",
"lifetime_per_file",
"lifetime_daily",
] {
assert_eq!(
db_row_count(&drip, tbl),
0,
"{tbl} should be empty after reset --all"
);
}
assert_eq!(
meter_total_reads(&drip),
0,
"meter after wipe must report 0 reads"
);
}
#[test]
fn reset_stats_zeros_lifetime_but_preserves_session_baselines() {
let drip = Drip::new();
let f = seed(&drip);
assert!(meter_total_reads(&drip) > 0);
let reads_before = db_row_count(&drip, "reads");
assert!(reads_before > 0, "must have a session row to preserve");
let o = drip.cmd().args(["reset", "--stats"]).output().unwrap();
assert!(o.status.success());
let out = String::from_utf8_lossy(&o.stdout);
assert!(out.contains("Lifetime counters cleared"), "{out}");
assert!(
out.contains("baselines untouched"),
"must reassure user that sessions survive: {out}"
);
assert_eq!(db_row_count(&drip, "lifetime_stats"), 0);
assert_eq!(db_row_count(&drip, "lifetime_per_file"), 0);
assert_eq!(db_row_count(&drip, "lifetime_daily"), 0);
assert_eq!(
db_row_count(&drip, "reads"),
reads_before,
"per-session reads MUST survive --stats"
);
let next = drip.read_stdout(&f);
assert!(
next.contains("[DRIP: unchanged since last read"),
"next read must still hit the unchanged path: {next}"
);
}
#[test]
fn reset_all_without_force_prompts_and_no_answer_aborts() {
let drip = Drip::new();
seed(&drip);
let reads_before = db_row_count(&drip, "reads");
let lifetime_before = db_row_count(&drip, "lifetime_stats");
assert!(reads_before > 0 && lifetime_before > 0);
let mut child = drip
.cmd()
.args(["reset", "--all"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
child.stdin.as_mut().unwrap().write_all(b"no\n").unwrap();
let o = child.wait_with_output().unwrap();
assert!(o.status.success(), "non-confirmed abort still exits 0");
let out = String::from_utf8_lossy(&o.stdout);
let err = String::from_utf8_lossy(&o.stderr);
assert!(
out.contains("Aborted"),
"stdout must announce the abort: stdout={out} stderr={err}"
);
assert!(
err.contains("Type 'yes' to confirm"),
"prompt must hit stderr: {err}"
);
assert_eq!(db_row_count(&drip, "reads"), reads_before);
assert_eq!(db_row_count(&drip, "lifetime_stats"), lifetime_before);
}
#[test]
fn reset_all_and_stats_together_is_a_clear_error() {
let drip = Drip::new();
let o = drip
.cmd()
.args(["reset", "--all", "--stats"])
.output()
.unwrap();
assert!(!o.status.success(), "must reject the conflicting combo");
let err = String::from_utf8_lossy(&o.stderr);
assert!(
err.contains("mutually exclusive"),
"error message must explain why: {err}"
);
}
#[test]
fn meter_flips_to_since_reset_label_after_reset_stats() {
let drip = Drip::new();
seed(&drip);
assert!(meter_total_reads(&drip) > 0);
let pre = drip
.cmd()
.arg("meter")
.env("NO_COLOR", "1")
.output()
.unwrap();
let pre_txt = String::from_utf8_lossy(&pre.stdout);
assert!(
pre_txt.contains("Since install:") || pre_txt.contains("Since Install"),
"fresh install should say 'Since install': {pre_txt}"
);
assert!(
!pre_txt.contains("Since reset:") && !pre_txt.contains("Since Reset"),
"pre-reset must not advertise reset: {pre_txt}"
);
let pre_json: serde_json::Value = serde_json::from_slice(
&drip
.cmd()
.args(["meter", "--json"])
.output()
.unwrap()
.stdout,
)
.unwrap();
assert!(
pre_json.get("last_reset_at").is_none(),
"last_reset_at must be absent before any reset: {pre_json}"
);
let r = drip.cmd().args(["reset", "--stats"]).output().unwrap();
assert!(r.status.success());
let post = drip
.cmd()
.arg("meter")
.env("NO_COLOR", "1")
.output()
.unwrap();
let post_txt = String::from_utf8_lossy(&post.stdout);
assert!(
post_txt.contains("Since reset:") || post_txt.contains("Since Reset"),
"after --stats the label must read 'Since reset': {post_txt}"
);
assert!(
!post_txt.contains("Since install:") && !post_txt.contains("Since Install"),
"after --stats the 'Since install' framing must be gone: {post_txt}"
);
let post_json: serde_json::Value = serde_json::from_slice(
&drip
.cmd()
.args(["meter", "--json"])
.output()
.unwrap()
.stdout,
)
.unwrap();
let ts = post_json["last_reset_at"]
.as_i64()
.expect("last_reset_at must be set after reset --stats");
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
assert!(
ts > 0 && ts <= now,
"last_reset_at ({ts}) must be a recent unix-second (now={now})"
);
}
#[test]
fn meter_flips_to_since_reset_label_after_reset_all() {
let drip = Drip::new();
seed(&drip);
let r = drip
.cmd()
.args(["reset", "--all", "--force"])
.output()
.unwrap();
assert!(r.status.success());
let json: serde_json::Value = serde_json::from_slice(
&drip
.cmd()
.args(["meter", "--json"])
.output()
.unwrap()
.stdout,
)
.unwrap();
assert!(
json.get("last_reset_at").is_some(),
"reset --all must stamp last_reset_at so the meter label flips: {json}"
);
let txt = String::from_utf8_lossy(
&drip
.cmd()
.arg("meter")
.env("NO_COLOR", "1")
.output()
.unwrap()
.stdout,
)
.to_string();
assert!(
txt.contains("Since reset:") || txt.contains("Since Reset"),
"after --all the human surface must read 'Since reset': {txt}"
);
}