use rusqlite::Connection;
use super::{DailyActivityStats, ProjectCount, SystemStats};
use crate::db::models::{AiUsageSourceTotals, AiUsageTotals, DailyAiUsage, WeeklyAiUsage};
use crate::db::query::{
query_ai_usage_source_totals, query_ai_usage_totals, query_daily_activity_stats,
query_daily_ai_usage, query_system_stats, query_top_projects, query_weekly_ai_usage,
};
fn setup_stats_schema(conn: &Connection) {
conn.execute_batch(
"CREATE TABLE memories (
id INTEGER PRIMARY KEY,
project TEXT NOT NULL,
status TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL
);
CREATE TABLE observations (
id INTEGER PRIMARY KEY,
project TEXT NOT NULL,
status TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL
);
CREATE TABLE session_summaries (
id INTEGER PRIMARY KEY
);
CREATE TABLE raw_messages (
id INTEGER PRIMARY KEY
);
CREATE TABLE captured_events (
id INTEGER PRIMARY KEY
);
CREATE TABLE extraction_tasks (
id INTEGER PRIMARY KEY,
status TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL
);
CREATE TABLE memory_candidates (
id INTEGER PRIMARY KEY,
review_status TEXT NOT NULL
);
CREATE TABLE pending_observations (
id INTEGER PRIMARY KEY,
status TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL DEFAULT 0,
next_retry_epoch INTEGER,
lease_owner TEXT,
lease_expires_epoch INTEGER
);
CREATE TABLE jobs (
id INTEGER PRIMARY KEY,
state TEXT NOT NULL,
lease_expires_epoch INTEGER
);
CREATE TABLE worker_heartbeats (
owner TEXT PRIMARY KEY,
pid INTEGER,
started_at_epoch INTEGER NOT NULL,
updated_at_epoch INTEGER NOT NULL
);
CREATE TABLE ai_usage_events (
id INTEGER PRIMARY KEY,
created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL,
project TEXT,
operation TEXT NOT NULL,
executor TEXT NOT NULL,
model TEXT,
input_tokens INTEGER NOT NULL,
output_tokens INTEGER NOT NULL,
reasoning_tokens INTEGER NOT NULL DEFAULT 0,
cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
cache_read_tokens INTEGER NOT NULL DEFAULT 0,
raw_input_tokens INTEGER NOT NULL DEFAULT 0,
raw_output_tokens INTEGER NOT NULL DEFAULT 0,
total_tokens INTEGER NOT NULL,
estimated_cost_usd REAL NOT NULL,
usage_source TEXT NOT NULL DEFAULT 'text_estimate',
pricing_source TEXT NOT NULL DEFAULT 'remem_static'
);",
)
.expect("schema should be created");
}
#[test]
fn query_system_stats_and_related_views_share_one_definition() {
let conn = Connection::open_in_memory().expect("in-memory db should open");
setup_stats_schema(&conn);
conn.execute(
"INSERT INTO memories (project, status, created_at_epoch) VALUES ('alpha', 'active', 200)",
[],
)
.expect("active memory insert should succeed");
conn.execute(
"INSERT INTO memories (project, status, created_at_epoch) VALUES ('alpha', 'archived', 150)",
[],
)
.expect("archived memory insert should succeed");
conn.execute(
"INSERT INTO memories (project, status, created_at_epoch) VALUES ('beta', 'active', 300)",
[],
)
.expect("second active memory insert should succeed");
conn.execute(
"INSERT INTO observations (project, status, created_at_epoch) VALUES ('alpha', 'active', 220)",
[],
)
.expect("active observation insert should succeed");
conn.execute(
"INSERT INTO observations (project, status, created_at_epoch) VALUES ('beta', 'stale', 140)",
[],
)
.expect("stale observation insert should succeed");
conn.execute("INSERT INTO session_summaries (id) VALUES (1)", [])
.expect("summary insert should succeed");
conn.execute("INSERT INTO captured_events (id) VALUES (1)", [])
.expect("captured event insert should succeed");
conn.execute(
"INSERT INTO extraction_tasks (status, created_at_epoch) VALUES ('pending', 90)",
[],
)
.expect("pending extraction task insert should succeed");
conn.execute(
"INSERT INTO extraction_tasks (status, created_at_epoch) VALUES ('processing', 95)",
[],
)
.expect("processing extraction task insert should succeed");
conn.execute(
"INSERT INTO extraction_tasks (status, created_at_epoch) VALUES ('failed', 96)",
[],
)
.expect("failed extraction task insert should succeed");
conn.execute(
"INSERT INTO memory_candidates (review_status) VALUES ('pending_review')",
[],
)
.expect("memory candidate insert should succeed");
conn.execute(
"INSERT INTO pending_observations (status, created_at_epoch) VALUES ('pending', 100)",
[],
)
.expect("pending insert should succeed");
conn.execute(
"INSERT INTO pending_observations (status, created_at_epoch) VALUES ('pending', 120)",
[],
)
.expect("second pending insert should succeed");
conn.execute(
"UPDATE pending_observations SET next_retry_epoch = strftime('%s', 'now') + 3600 WHERE id = 2",
[],
)
.expect("delayed pending update should succeed");
conn.execute(
"INSERT INTO pending_observations (status, created_at_epoch, lease_owner, lease_expires_epoch)
VALUES ('processing', 130, 'worker-a', strftime('%s', 'now') - 1)",
[],
)
.expect("processing pending insert should succeed");
conn.execute(
"INSERT INTO pending_observations (status, created_at_epoch) VALUES ('failed', 140)",
[],
)
.expect("failed pending insert should succeed");
conn.execute(
"INSERT INTO jobs (state, lease_expires_epoch) VALUES ('pending', NULL)",
[],
)
.expect("pending job insert should succeed");
conn.execute(
"INSERT INTO jobs (state, lease_expires_epoch) VALUES ('processing', 0)",
[],
)
.expect("stuck job insert should succeed");
conn.execute(
"INSERT INTO jobs (state, lease_expires_epoch) VALUES ('failed', NULL)",
[],
)
.expect("failed job insert should succeed");
conn.execute(
"INSERT INTO worker_heartbeats (owner, pid, started_at_epoch, updated_at_epoch)
VALUES ('worker-a', ?1, strftime('%s', 'now') - 10, strftime('%s', 'now') - 10)",
[i64::from(std::process::id())],
)
.expect("heartbeat insert should succeed");
let system = query_system_stats(&conn).expect("system stats should load");
assert_eq!(
system,
SystemStats {
active_memories: 2,
active_observations: 1,
session_summaries: 1,
raw_messages: 0,
captured_events: 1,
pending_extraction_tasks: 1,
processing_extraction_tasks: 1,
failed_extraction_tasks: 1,
oldest_pending_extraction_epoch: Some(90),
pending_memory_candidates: 1,
pending_observations: 2,
ready_pending_observations: 1,
delayed_pending_observations: 1,
processing_pending_observations: 1,
expired_processing_pending_observations: 1,
failed_pending_observations: 1,
oldest_ready_pending_epoch: Some(100),
pending_jobs: 1,
processing_jobs: 1,
failed_jobs: 1,
stuck_jobs: 1,
worker_daemon_healthy: true,
worker_heartbeat_owner: Some("worker-a".to_string()),
worker_heartbeat_age_secs: system.worker_heartbeat_age_secs,
}
);
assert!(
system.worker_heartbeat_age_secs.unwrap_or_default() <= 20,
"heartbeat age should be recent"
);
let daily = query_daily_activity_stats(&conn, 180).expect("daily stats should load");
assert_eq!(
daily,
DailyActivityStats {
memories: 2,
observations: 1,
}
);
let top_projects = query_top_projects(&conn, 5).expect("top projects should load");
assert_eq!(
top_projects,
vec![
ProjectCount {
project: "alpha".to_string(),
count: 1,
},
ProjectCount {
project: "beta".to_string(),
count: 1,
},
]
);
}
fn insert_usage(
conn: &Connection,
project: &str,
created_at_epoch: i64,
input_tokens: i64,
output_tokens: i64,
reasoning_tokens: i64,
cache_read_tokens: i64,
estimated_cost_usd: f64,
) {
conn.execute(
"INSERT INTO ai_usage_events
(created_at, created_at_epoch, project, operation, executor, model,
input_tokens, output_tokens, reasoning_tokens, cache_read_tokens, total_tokens,
estimated_cost_usd, usage_source, pricing_source)
VALUES ('2026-01-01T00:00:00Z', ?1, ?2, 'summary', 'codex-cli', 'codex-default',
?3, ?4, ?5, ?6, ?7, ?8, 'codex_log', 'remem_static')",
rusqlite::params![
created_at_epoch,
project,
input_tokens,
output_tokens,
reasoning_tokens,
cache_read_tokens,
input_tokens + output_tokens + reasoning_tokens + cache_read_tokens,
estimated_cost_usd
],
)
.expect("usage insert should succeed");
}
#[test]
fn query_ai_usage_groups_daily_and_weekly_token_costs() {
let conn = Connection::open_in_memory().expect("in-memory db should open");
setup_stats_schema(&conn);
let jan_05_2026 = 1_767_571_200;
let jan_06_2026 = 1_767_657_600;
let jan_12_2026 = 1_768_176_000;
insert_usage(&conn, "alpha", jan_05_2026, 100, 40, 10, 50, 0.001);
insert_usage(&conn, "alpha", jan_05_2026 + 60, 200, 60, 20, 80, 0.002);
insert_usage(&conn, "alpha", jan_06_2026, 300, 80, 30, 120, 0.003);
insert_usage(&conn, "beta", jan_12_2026, 500, 100, 40, 160, 0.005);
let alpha_totals = query_ai_usage_totals(&conn, Some(jan_05_2026), Some("alpha"))
.expect("usage totals should load");
assert_eq!(
alpha_totals,
AiUsageTotals {
calls: 3,
input_tokens: 600,
output_tokens: 180,
reasoning_tokens: 60,
cache_creation_tokens: 0,
cache_read_tokens: 250,
total_tokens: 1090,
estimated_cost_usd: 0.006,
}
);
let alpha_sources = query_ai_usage_source_totals(&conn, Some(jan_05_2026), Some("alpha"))
.expect("usage source totals should load");
assert_eq!(
alpha_sources,
vec![AiUsageSourceTotals {
usage_source: "codex_log".to_string(),
pricing_source: "remem_static".to_string(),
calls: 3,
total_tokens: 1090,
estimated_cost_usd: 0.006,
}]
);
let daily = query_daily_ai_usage(&conn, jan_05_2026, Some("alpha"), 14)
.expect("daily usage should load");
assert_eq!(
daily,
vec![
DailyAiUsage {
day: "2026-01-06".to_string(),
calls: 1,
input_tokens: 300,
output_tokens: 80,
reasoning_tokens: 30,
cache_creation_tokens: 0,
cache_read_tokens: 120,
total_tokens: 530,
estimated_cost_usd: 0.003,
},
DailyAiUsage {
day: "2026-01-05".to_string(),
calls: 2,
input_tokens: 300,
output_tokens: 100,
reasoning_tokens: 30,
cache_creation_tokens: 0,
cache_read_tokens: 130,
total_tokens: 560,
estimated_cost_usd: 0.003,
},
]
);
let weekly =
query_weekly_ai_usage(&conn, jan_05_2026, None, 8).expect("weekly usage should load");
assert_eq!(
weekly,
vec![
WeeklyAiUsage {
week: "2026-W02".to_string(),
calls: 1,
input_tokens: 500,
output_tokens: 100,
reasoning_tokens: 40,
cache_creation_tokens: 0,
cache_read_tokens: 160,
total_tokens: 800,
estimated_cost_usd: 0.005,
},
WeeklyAiUsage {
week: "2026-W01".to_string(),
calls: 3,
input_tokens: 600,
output_tokens: 180,
reasoning_tokens: 60,
cache_creation_tokens: 0,
cache_read_tokens: 250,
total_tokens: 1090,
estimated_cost_usd: 0.006,
},
]
);
}