mod common;
use common::{llman_command, prepare_work_and_config_dirs, run_llman};
use rusqlite::Connection;
use serde_json::Value;
use std::fs;
use tempfile::TempDir;
fn create_state_db(path: &std::path::Path) {
let conn = Connection::open(path).expect("open sqlite");
conn.execute(
r#"
CREATE TABLE threads (
id TEXT PRIMARY KEY,
rollout_path TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
cwd TEXT NOT NULL,
title TEXT,
tokens_used INTEGER NOT NULL DEFAULT 0
);
"#,
(),
)
.expect("create threads table");
}
fn insert_thread(
conn: &Connection,
id: &str,
cwd: &str,
tokens_used: i64,
rollout_path: Option<&str>,
) {
conn.execute(
"INSERT INTO threads (id, rollout_path, created_at, updated_at, cwd, title, tokens_used) VALUES (?1, ?2, 1, 2, ?3, 't', ?4);",
(id, rollout_path, cwd, tokens_used),
)
.expect("insert thread");
}
#[test]
fn codex_stats_summary_json_filters_by_cwd() {
let temp = TempDir::new().expect("temp dir");
let (work_dir, config_dir) = prepare_work_and_config_dirs(temp.path());
let db_path = temp.path().join("state_1.sqlite");
create_state_db(&db_path);
{
let conn = Connection::open(&db_path).expect("open sqlite");
insert_thread(&conn, "t1", &work_dir.to_string_lossy(), 9, None);
insert_thread(&conn, "t2", "/other", 100, None);
}
let output = run_llman(
&[
"x",
"codex",
"stats",
"--state-db",
db_path.to_str().unwrap(),
"--format",
"json",
],
&work_dir,
&config_dir,
);
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let v: Value = serde_json::from_slice(&output.stdout).expect("parse json");
assert_eq!(v["tool"], "codex");
assert_eq!(v["view"], "summary");
assert_eq!(v["result"]["coverage"]["total_sessions"], 1);
assert_eq!(v["result"]["totals"]["tokens_total_known"], 9);
}
#[test]
fn codex_stats_session_json_with_breakdown() {
let temp = TempDir::new().expect("temp dir");
let (work_dir, config_dir) = prepare_work_and_config_dirs(temp.path());
let rollout = temp.path().join("rollout.jsonl");
fs::write(
&rollout,
r#"{"type":"event_msg","payload":{"type":"token_count","info":{"total_token_usage":{"input_tokens":1,"cached_input_tokens":2,"output_tokens":3,"reasoning_output_tokens":4,"total_tokens":10}}}}"#,
)
.expect("write rollout");
let db_path = temp.path().join("state_1.sqlite");
create_state_db(&db_path);
{
let conn = Connection::open(&db_path).expect("open sqlite");
insert_thread(
&conn,
"t1",
&work_dir.to_string_lossy(),
9,
Some(&rollout.to_string_lossy()),
);
}
let output = run_llman(
&[
"x",
"codex",
"stats",
"--state-db",
db_path.to_str().unwrap(),
"--with-breakdown",
"--view",
"session",
"--id",
"t1",
"--format",
"json",
],
&work_dir,
&config_dir,
);
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let v: Value = serde_json::from_slice(&output.stdout).expect("parse json");
assert_eq!(v["view"], "session");
assert_eq!(v["result"]["session"]["id"], "t1");
assert_eq!(v["result"]["session"]["token_usage"]["total"], 9);
assert_eq!(v["result"]["session"]["token_usage"]["input"], 1);
assert_eq!(v["result"]["session"]["token_usage"]["cache"], 2);
assert_eq!(v["result"]["session"]["token_usage"]["output"], 3);
assert_eq!(v["result"]["session"]["token_usage"]["reasoning"], 4);
}
#[test]
fn codex_stats_table_color_auto_has_no_ansi_when_captured() {
let temp = TempDir::new().expect("temp dir");
let (work_dir, config_dir) = prepare_work_and_config_dirs(temp.path());
let db_path = temp.path().join("state_1.sqlite");
create_state_db(&db_path);
{
let conn = Connection::open(&db_path).expect("open sqlite");
insert_thread(&conn, "t1", &work_dir.to_string_lossy(), 9, None);
}
let output = run_llman(
&[
"x",
"codex",
"stats",
"--state-db",
db_path.to_str().unwrap(),
"--format",
"table",
"--color",
"auto",
],
&work_dir,
&config_dir,
);
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(
!output.stdout.windows(2).any(|w| w == b"\x1b["),
"expected no ANSI escape sequences in captured output, got:\n{}",
String::from_utf8_lossy(&output.stdout)
);
}
#[test]
fn codex_stats_table_color_always_includes_ansi_when_captured() {
let temp = TempDir::new().expect("temp dir");
let (work_dir, config_dir) = prepare_work_and_config_dirs(temp.path());
let db_path = temp.path().join("state_1.sqlite");
create_state_db(&db_path);
{
let conn = Connection::open(&db_path).expect("open sqlite");
insert_thread(&conn, "t1", &work_dir.to_string_lossy(), 9, None);
}
let output = run_llman(
&[
"x",
"codex",
"stats",
"--state-db",
db_path.to_str().unwrap(),
"--format",
"table",
"--color",
"always",
],
&work_dir,
&config_dir,
);
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(
output.stdout.windows(2).any(|w| w == b"\x1b["),
"expected ANSI escape sequences with --color always, got:\n{}",
String::from_utf8_lossy(&output.stdout)
);
}
#[test]
fn codex_stats_json_is_never_colored() {
let temp = TempDir::new().expect("temp dir");
let (work_dir, config_dir) = prepare_work_and_config_dirs(temp.path());
let db_path = temp.path().join("state_1.sqlite");
create_state_db(&db_path);
{
let conn = Connection::open(&db_path).expect("open sqlite");
insert_thread(&conn, "t1", &work_dir.to_string_lossy(), 9, None);
}
let output = run_llman(
&[
"x",
"codex",
"stats",
"--state-db",
db_path.to_str().unwrap(),
"--format",
"json",
"--color",
"always",
],
&work_dir,
&config_dir,
);
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(
!output.stdout.windows(2).any(|w| w == b"\x1b["),
"expected no ANSI escape sequences in json output, got:\n{}",
String::from_utf8_lossy(&output.stdout)
);
let v: Value = serde_json::from_slice(&output.stdout).expect("parse json");
assert_eq!(v["tool"], "codex");
}
#[test]
#[cfg(unix)]
fn codex_stats_table_no_color_env_disables_ansi_in_tty_auto_mode() {
use expectrl::Session;
use expectrl::process::unix::WaitStatus;
use std::io::Read;
let temp = TempDir::new().expect("temp dir");
let (work_dir, config_dir) = prepare_work_and_config_dirs(temp.path());
let db_path = temp.path().join("state_1.sqlite");
create_state_db(&db_path);
{
let conn = Connection::open(&db_path).expect("open sqlite");
insert_thread(&conn, "t1", &work_dir.to_string_lossy(), 9, None);
}
let mut cmd = llman_command(&config_dir);
cmd.env("NO_COLOR", "1")
.args([
"x",
"codex",
"stats",
"--state-db",
db_path.to_str().unwrap(),
"--format",
"table",
"--color",
"auto",
])
.current_dir(&work_dir);
let mut session = Session::spawn(cmd).expect("spawn llman in pty");
let mut buf = Vec::new();
session.read_to_end(&mut buf).expect("read stdout");
assert_eq!(
session.get_process().wait().expect("wait"),
WaitStatus::Exited(session.get_process().pid(), 0)
);
assert!(
!buf.windows(2).any(|w| w == b"\x1b["),
"expected no ANSI escape sequences with NO_COLOR=1 in tty auto mode, got:\n{}",
String::from_utf8_lossy(&buf)
);
}