use assert_cmd::cargo::cargo_bin_cmd;
use assert_cmd::Command;
use predicates::prelude::*;
use std::fs;
use std::path::Path;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use tempfile::TempDir;
fn prime_pricing_cache(base: &Path) {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time before unix epoch")
.as_secs();
let payload = format!(r#"{{"timestamp":{},"data":{{}}}}"#, now);
let dir = base.join(".config/tokmesh/cache");
fs::create_dir_all(&dir).unwrap();
fs::write(dir.join("pricing-litellm.json"), &payload).unwrap();
fs::write(dir.join("pricing-openrouter.json"), &payload).unwrap();
}
fn prime_override_pricing_cache(config_dir: &Path) {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time before unix epoch")
.as_secs();
let payload = format!(r#"{{"timestamp":{},"data":{{}}}}"#, now);
let cache_dir = config_dir.join("cache");
fs::create_dir_all(&cache_dir).unwrap();
fs::write(cache_dir.join("pricing-litellm.json"), &payload).unwrap();
fs::write(cache_dir.join("pricing-openrouter.json"), &payload).unwrap();
}
fn create_temp_fixture_dir_with_pricing_cache(with_pricing_cache: bool) -> TempDir {
let tmp = TempDir::new().expect("failed to create temp dir");
let base = tmp.path();
if with_pricing_cache {
prime_pricing_cache(base);
}
let session1 = base.join(".local/share/opencode/storage/message/session1");
fs::create_dir_all(&session1).unwrap();
let msg_a = r#"{
"id": "msg_a",
"sessionID": "session1",
"role": "assistant",
"modelID": "claude-sonnet-4-20250514",
"providerID": "anthropic",
"cost": 0.05,
"tokens": {
"input": 1000,
"output": 500,
"reasoning": 0,
"cache": { "read": 200, "write": 50 }
},
"time": { "created": 1718452800000.0, "completed": 1718452803500.0 }
}"#;
fs::write(session1.join("msg_a.json"), msg_a).unwrap();
let msg_b = r#"{
"id": "msg_b",
"sessionID": "session1",
"role": "assistant",
"modelID": "claude-sonnet-4-20250514",
"providerID": "anthropic",
"cost": 0.03,
"tokens": {
"input": 800,
"output": 300,
"reasoning": 0,
"cache": { "read": 150, "write": 30 }
},
"time": { "created": 1718456400000.0, "completed": 1718456402560.0 }
}"#;
fs::write(session1.join("msg_b.json"), msg_b).unwrap();
let session2 = base.join(".local/share/opencode/storage/message/session2");
fs::create_dir_all(&session2).unwrap();
let msg_c = r#"{
"id": "msg_c",
"sessionID": "session2",
"role": "assistant",
"modelID": "gpt-4o",
"providerID": "openai",
"cost": 0.02,
"tokens": {
"input": 600,
"output": 200,
"reasoning": 0,
"cache": { "read": 100, "write": 20 }
},
"time": { "created": 1736510400000.0, "completed": 1736510400920.0 }
}"#;
fs::write(session2.join("msg_c.json"), msg_c).unwrap();
tmp
}
fn create_temp_fixture_dir() -> TempDir {
create_temp_fixture_dir_with_pricing_cache(true)
}
fn create_fake_codex_bin() -> TempDir {
let tmp = TempDir::new().expect("failed to create fake codex dir");
let codex_path = tmp.path().join("codex");
fs::write(
&codex_path,
r#"#!/bin/sh
case "$TOKMESH_FAKE_CODEX_MODE" in
success)
printf 'captured ok'
exit 0
;;
fail)
printf 'captured fail'
exit 17
;;
slow)
exec sleep 20
;;
*)
echo "unknown TOKMESH_FAKE_CODEX_MODE" >&2
exit 2
;;
esac
"#,
)
.unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut permissions = fs::metadata(&codex_path).unwrap().permissions();
permissions.set_mode(0o755);
fs::set_permissions(&codex_path, permissions).unwrap();
}
tmp
}
fn headless_capture_command(fake_bin: &Path, output_path: &Path, mode: &str) -> Command {
let mut cmd = cargo_bin_cmd!("tokmesh");
let path = std::env::var_os("PATH").unwrap_or_default();
let joined_path = std::env::join_paths(
std::iter::once(fake_bin.to_path_buf()).chain(std::env::split_paths(&path)),
)
.unwrap();
cmd.env("HOME", fake_bin)
.env("TOKMESH_FAKE_CODEX_MODE", mode)
.env("TOKMESH_NATIVE_TIMEOUT_MS", "10000")
.env("PATH", joined_path)
.args([
"headless",
"--output",
output_path.to_str().unwrap(),
"--no-auto-flags",
"codex",
]);
cmd
}
#[test]
fn headless_capture_fast_success_does_not_wait_for_timeout() {
let fake_bin = create_fake_codex_bin();
let output_path = fake_bin.path().join("success.jsonl");
let started = Instant::now();
headless_capture_command(fake_bin.path(), &output_path, "success")
.assert()
.success();
let elapsed = started.elapsed();
assert!(
elapsed < Duration::from_secs(8),
"fast success waited too long: {elapsed:?}"
);
assert_eq!(fs::read_to_string(output_path).unwrap(), "captured ok");
}
#[test]
fn headless_capture_fast_nonzero_preserves_exit_code() {
let fake_bin = create_fake_codex_bin();
let output_path = fake_bin.path().join("fail.jsonl");
let started = Instant::now();
headless_capture_command(fake_bin.path(), &output_path, "fail")
.assert()
.failure()
.code(17);
let elapsed = started.elapsed();
assert!(
elapsed < Duration::from_secs(8),
"fast failure waited too long: {elapsed:?}"
);
assert_eq!(fs::read_to_string(output_path).unwrap(), "captured fail");
}
#[test]
fn headless_capture_slow_command_times_out() {
let fake_bin = create_fake_codex_bin();
let output_path = fake_bin.path().join("slow.jsonl");
let started = Instant::now();
headless_capture_command(fake_bin.path(), &output_path, "slow")
.assert()
.failure()
.code(124);
let elapsed = started.elapsed();
assert!(
elapsed >= Duration::from_secs(10) && elapsed < Duration::from_secs(14),
"slow command timeout duration was unexpected: {elapsed:?}"
);
}
fn create_temp_fixture_dir_without_pricing_cache() -> TempDir {
create_temp_fixture_dir_with_pricing_cache(false)
}
fn create_empty_fixture_dir() -> TempDir {
let tmp = TempDir::new().expect("failed to create temp dir");
let base = tmp.path();
prime_pricing_cache(base);
let opencode_dir = base.join(".local/share/opencode/storage/message");
fs::create_dir_all(opencode_dir).unwrap();
tmp
}
fn create_opencodereview_fixture_dir() -> TempDir {
let tmp = TempDir::new().expect("failed to create temp dir");
let base = tmp.path();
prime_pricing_cache(base);
let session_dir = base.join(".opencodereview/sessions/test-repo");
fs::create_dir_all(&session_dir).unwrap();
fs::write(
session_dir.join("session-1.jsonl"),
concat!(
r#"{"type":"session_start","sessionId":"session-1","timestamp":"2026-01-15T10:00:00Z","cwd":"/workspace/project","model":"gpt-4o"}"#,
"\n",
r#"{"type":"llm_response","sessionId":"session-1","timestamp":"2026-01-15T10:00:05Z","model":"gpt-4o","duration_ms":1500,"usage":{"prompt_tokens":1000,"completion_tokens":250,"cache_read_tokens":100,"cache_write_tokens":50}}"#,
"\n"
),
)
.unwrap();
tmp
}
fn create_timezone_boundary_fixture_dir() -> TempDir {
let tmp = TempDir::new().expect("failed to create temp dir");
let base = tmp.path();
prime_pricing_cache(base);
let session = base.join(".local/share/opencode/storage/message/session1");
fs::create_dir_all(&session).unwrap();
let msg_a = r#"{
"id": "msg_a",
"sessionID": "session1",
"role": "assistant",
"modelID": "claude-sonnet-4-20250514",
"providerID": "anthropic",
"cost": 0.05,
"tokens": {
"input": 1000,
"output": 500,
"reasoning": 0,
"cache": { "read": 200, "write": 50 }
},
"time": { "created": 1772474400000.0 }
}"#;
fs::write(session.join("msg_a.json"), msg_a).unwrap();
let msg_b = r#"{
"id": "msg_b",
"sessionID": "session1",
"role": "assistant",
"modelID": "claude-sonnet-4-20250514",
"providerID": "anthropic",
"cost": 0.03,
"tokens": {
"input": 800,
"output": 300,
"reasoning": 0,
"cache": { "read": 150, "write": 30 }
},
"time": { "created": 1772512200000.0 }
}"#;
fs::write(session.join("msg_b.json"), msg_b).unwrap();
tmp
}
fn create_positive_utc_offset_submit_fixture_dir() -> (TempDir, String) {
let tmp = TempDir::new().expect("failed to create temp dir");
let base = tmp.path();
prime_pricing_cache(base);
let utc_today = chrono::Utc::now().date_naive();
let utc_noon = utc_today.and_hms_opt(12, 0, 0).unwrap().and_utc();
let local_date = utc_today.succ_opt().unwrap().format("%Y-%m-%d").to_string();
let session = base.join(".local/share/opencode/storage/message/session1");
fs::create_dir_all(&session).unwrap();
let message = serde_json::json!({
"id": "msg_ahead_of_utc",
"sessionID": "session1",
"role": "assistant",
"modelID": "gpt-4o",
"providerID": "openai",
"cost": 0.02,
"tokens": {
"input": 1000,
"output": 500,
"reasoning": 0,
"cache": { "read": 200, "write": 50 }
},
"time": { "created": utc_noon.timestamp_millis() as f64 }
});
fs::write(session.join("msg_ahead_of_utc.json"), message.to_string()).unwrap();
(tmp, local_date)
}
fn create_qwen_workspace_fixture_dir() -> TempDir {
let tmp = TempDir::new().expect("failed to create temp dir");
let base = tmp.path();
prime_pricing_cache(base);
let session = base.join(".qwen/projects/demo-workspace/chats");
fs::create_dir_all(&session).unwrap();
let msg = r#"{"type":"assistant","model":"qwen3.5-plus","timestamp":"2026-02-23T14:24:56.857Z","sessionId":"demo-session","usageMetadata":{"promptTokenCount":12414,"candidatesTokenCount":76,"thoughtsTokenCount":39,"cachedContentTokenCount":0}}"#;
fs::write(session.join("session-1.jsonl"), msg).unwrap();
tmp
}
fn create_codex_fixture_dir() -> TempDir {
let tmp = TempDir::new().expect("failed to create temp dir");
let base = tmp.path();
prime_pricing_cache(base);
let sessions_dir = base.join(".codex/sessions");
fs::create_dir_all(&sessions_dir).unwrap();
fs::write(
sessions_dir.join("session-1.jsonl"),
concat!(
r#"{"type":"turn_context","payload":{"model":"gpt-4o-mini"}}"#,
"\n",
r#"{"timestamp":"2026-01-01T00:00:01Z","type":"event_msg","payload":{"type":"token_count","info":{"last_token_usage":{"input_tokens":120,"cached_input_tokens":20,"output_tokens":30}}}}"#,
"\n"
),
)
.unwrap();
tmp
}
fn create_codex_workspace_fixture_dir() -> TempDir {
let tmp = TempDir::new().expect("failed to create temp dir");
let base = tmp.path();
prime_pricing_cache(base);
let sessions_dir = base.join(".codex/sessions");
fs::create_dir_all(&sessions_dir).unwrap();
fs::write(
sessions_dir.join("workspace-session.jsonl"),
concat!(
r#"{"type":"session_meta","payload":{"source":"chat","cwd":"/Users/alice/codex-workspace"}}"#,
"\n",
r#"{"type":"turn_context","payload":{"model":"gpt-5.4"}}"#,
"\n",
r#"{"timestamp":"2026-01-01T00:00:01Z","type":"event_msg","payload":{"type":"token_count","info":{"last_token_usage":{"input_tokens":120,"cached_input_tokens":20,"output_tokens":30}}}}"#,
"\n"
),
)
.unwrap();
tmp
}
fn create_opencode_workspace_fixture_dir() -> TempDir {
let tmp = TempDir::new().expect("failed to create temp dir");
let base = tmp.path();
prime_pricing_cache(base);
let session = base.join(".local/share/opencode/storage/message/workspace-session");
fs::create_dir_all(&session).unwrap();
let msg = r#"{
"id": "workspace_msg",
"sessionID": "workspace-session",
"role": "assistant",
"modelID": "claude-sonnet-4-20250514",
"providerID": "anthropic",
"cost": 0.05,
"tokens": {
"input": 1000,
"output": 500,
"reasoning": 0,
"cache": { "read": 200, "write": 50 }
},
"time": { "created": 1718452800000.0 },
"path": { "root": "/Users/alice/opencode-workspace" }
}"#;
fs::write(session.join("workspace_msg.json"), msg).unwrap();
tmp
}
fn create_conflicting_opencode_fixture_dir() -> TempDir {
let tmp = TempDir::new().expect("failed to create temp dir");
let base = tmp.path();
prime_pricing_cache(base);
let session = base.join(".local/share/opencode/storage/message/conflicting-session");
fs::create_dir_all(&session).unwrap();
let msg = r#"{
"id": "conflict_msg",
"sessionID": "conflicting-session",
"role": "assistant",
"modelID": "gemini-2.5-pro",
"providerID": "google",
"cost": 0.11,
"tokens": {
"input": 111,
"output": 222,
"reasoning": 0,
"cache": { "read": 0, "write": 0 }
},
"time": { "created": 1736510400000.0 }
}"#;
fs::write(session.join("conflict_msg.json"), msg).unwrap();
tmp
}
fn create_conflicting_codex_fixture_dir() -> TempDir {
let tmp = TempDir::new().expect("failed to create temp dir");
let base = tmp.path();
prime_pricing_cache(base);
let sessions_dir = base.join(".codex/sessions");
fs::create_dir_all(&sessions_dir).unwrap();
fs::write(
sessions_dir.join("conflicting-session.jsonl"),
concat!(
r#"{"type":"turn_context","payload":{"model":"gpt-5"}}"#,
"\n",
r#"{"timestamp":"2026-01-01T00:00:01Z","type":"event_msg","payload":{"type":"token_count","info":{"last_token_usage":{"input_tokens":900,"cached_input_tokens":90,"output_tokens":45}}}}"#,
"\n"
),
)
.unwrap();
tmp
}
fn cmd_with_home(tmp: &Path) -> Command {
let mut cmd = cargo_bin_cmd!("tokmesh");
cmd.env("HOME", tmp)
.env("XDG_CONFIG_HOME", tmp.join(".config"))
.env("XDG_DATA_HOME", tmp.join(".local/share"))
.env("XDG_CACHE_HOME", tmp.join(".cache"))
.env("TOKMESH_PRICING_CACHE_ONLY", "1")
.env_remove("TOKMESH_EXTRA_DIRS")
.env_remove("TOKMESH_HEADLESS_DIR")
.env_remove("CODEX_HOME")
.env_remove("COPILOT_OTEL_FILE_EXPORTER_PATH")
.env_remove("GOOSE_PATH_ROOT")
.env_remove("CODEBUFF_DATA_DIR")
.env_remove("GEMINI_CLI_HOME")
.env_remove("HERMES_HOME")
.env_remove("TOKMESH_CONFIG_DIR");
cmd
}
fn leaderboard_cmd_with_home(tmp: &Path) -> Command {
let mut cmd = cmd_with_home(tmp);
cmd.env("TOKMESH_CONFIG_DIR", tmp.join(".config/tokmesh"))
.env_remove("TOKMESH_TOKSCALE_API_TOKEN")
.env_remove("TOKMESH_TOKENSCI_API_TOKEN")
.env_remove("TOKSCALE_API_TOKEN")
.env_remove("TOKENS_API_TOKEN");
cmd
}
fn cmd_with_conflicting_env(tmp: &Path) -> Command {
let mut cmd = cargo_bin_cmd!("tokmesh");
cmd.env("HOME", tmp)
.env("XDG_CONFIG_HOME", tmp.join(".config"))
.env("XDG_DATA_HOME", tmp.join(".local/share"))
.env("XDG_CACHE_HOME", tmp.join(".cache"));
cmd
}
fn offline_cmd_with_home(tmp: &Path) -> Command {
let mut cmd = cargo_bin_cmd!("tokmesh");
cmd.env("HOME", tmp)
.env("XDG_CONFIG_HOME", tmp.join(".config"))
.env("XDG_DATA_HOME", tmp.join(".local/share"))
.env("XDG_CACHE_HOME", tmp.join(".cache"))
.env("HTTP_PROXY", "http://127.0.0.1:9")
.env("HTTPS_PROXY", "http://127.0.0.1:9")
.env("ALL_PROXY", "http://127.0.0.1:9")
.env_remove("TOKMESH_EXTRA_DIRS")
.env_remove("TOKMESH_HEADLESS_DIR")
.env_remove("CODEX_HOME")
.env_remove("COPILOT_OTEL_FILE_EXPORTER_PATH")
.env_remove("GOOSE_PATH_ROOT")
.env_remove("CODEBUFF_DATA_DIR")
.env_remove("GEMINI_CLI_HOME")
.env_remove("HERMES_HOME")
.env_remove("TOKMESH_CONFIG_DIR");
cmd
}
fn write_pricing_cache(base: &Path, timestamp: u64) {
let litellm = format!(
r#"{{"timestamp":{},"data":{{"gpt-4o":{{"input_cost_per_token":0.0000025,"output_cost_per_token":0.00001}},"claude-sonnet-4-20250514":{{"input_cost_per_token":0.000003,"output_cost_per_token":0.000015}}}}}}"#,
timestamp
);
let openrouter = format!(r#"{{"timestamp":{},"data":{{}}}}"#, timestamp);
let dir = base.join(".config/tokmesh/cache");
fs::create_dir_all(&dir).unwrap();
fs::write(dir.join("pricing-litellm.json"), &litellm).unwrap();
fs::write(dir.join("pricing-openrouter.json"), &openrouter).unwrap();
}
fn add_uncosted_opencode_message(base: &Path) {
let session2 = base.join(".local/share/opencode/storage/message/session2");
fs::create_dir_all(&session2).unwrap();
let msg_d = r#"{
"id": "msg_d",
"sessionID": "session2",
"role": "assistant",
"modelID": "gpt-4o",
"providerID": "openai",
"tokens": {
"input": 1000,
"output": 400,
"reasoning": 0,
"cache": { "read": 0, "write": 0 }
},
"time": { "created": 1736510460000.0 }
}"#;
fs::write(session2.join("msg_d.json"), msg_d).unwrap();
}
fn add_zero_usage_opencode_message(base: &Path) {
let session3 = base.join(".local/share/opencode/storage/message/session3");
fs::create_dir_all(&session3).unwrap();
let msg_z = r#"{
"id": "msg_z",
"sessionID": "session3",
"role": "assistant",
"modelID": "zero-model",
"providerID": "openai",
"cost": 0.0,
"tokens": {
"input": 0,
"output": 0,
"reasoning": 0,
"cache": { "read": 0, "write": 0 }
},
"time": { "created": 1678881600000.0 }
}"#;
fs::write(session3.join("msg_z.json"), msg_z).unwrap();
}
fn write_fireworks_pricing_cache(base: &Path) {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time before unix epoch")
.as_secs();
let litellm = serde_json::json!({
"timestamp": now,
"data": {
"fireworks_ai/accounts/fireworks/models/deepseek-r1-0528-distill-qwen3-8b": {
"input_cost_per_token": 0.0000002,
"output_cost_per_token": 0.0000002
}
}
});
let openrouter = serde_json::json!({
"timestamp": now,
"data": {
"deepseek/deepseek-v4-pro": {
"input_cost_per_token": 0.000001,
"output_cost_per_token": 0.000002
}
}
});
let dir = base.join(".config/tokmesh/cache");
fs::create_dir_all(&dir).unwrap();
fs::write(
dir.join("pricing-litellm.json"),
serde_json::to_vec(&litellm).unwrap(),
)
.unwrap();
fs::write(
dir.join("pricing-openrouter.json"),
serde_json::to_vec(&openrouter).unwrap(),
)
.unwrap();
}
fn write_fake_credentials(base: &Path) {
let body = r#"{"token":"fake","username":"testuser","createdAt":"2024-01-01T00:00:00Z"}"#;
for board in ["tokscale", "tokensci"] {
let creds_dir = base.join(".config/tokmesh").join(board);
fs::create_dir_all(&creds_dir).unwrap();
fs::write(creds_dir.join("credentials.json"), body).unwrap();
}
}
fn write_settings_json(base: &Path, body: &str) {
let path = settings_json_path(base);
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path, body).unwrap();
}
fn settings_json_path(base: &Path) -> std::path::PathBuf {
if cfg!(target_os = "windows") {
base.join("AppData")
.join("Roaming")
.join("tokmesh")
.join("settings.json")
} else {
base.join(".config").join("tokmesh").join("settings.json")
}
}
fn write_clawdboard_export_fixture(dir: &Path) -> std::path::PathBuf {
let path = dir.join("export.json");
fs::write(
&path,
r#"{
"dailyAggregates": [
{
"date": "2026-05-11",
"source": "codex",
"machineId": "m1",
"inputTokens": 100,
"outputTokens": 50,
"cacheCreationTokens": 0,
"cacheReadTokens": 10,
"totalCost": "0.50",
"modelsUsed": ["gpt-5.5"],
"modelBreakdowns": [
{ "modelName": "gpt-5.5", "cost": 0.5, "inputTokens": 100,
"outputTokens": 50, "cacheReadTokens": 10, "cacheCreationTokens": 0 }
]
}
]
}"#,
)
.unwrap();
path
}
fn write_local_mcp_config(home: &Path) {
let dir = home.join(".claude");
fs::create_dir_all(&dir).unwrap();
fs::write(
dir.join(".mcp.json"),
r#"{"mcpServers":{"local-only-test-server":{"command":"echo"}}}"#,
)
.unwrap();
}
fn write_codex_token_session(dir: &Path, name: &str, model: &str, input: i64, output: i64) {
fs::create_dir_all(dir).unwrap();
let turn_context = serde_json::json!({
"type": "turn_context",
"payload": {
"model": model
}
});
let token_count = serde_json::json!({
"timestamp": "2026-01-01T00:00:01Z",
"type": "event_msg",
"payload": {
"type": "token_count",
"info": {
"last_token_usage": {
"input_tokens": input,
"cached_input_tokens": 0,
"output_tokens": output
}
}
}
});
fs::write(
dir.join(name),
format!("{}\n{}\n", turn_context, token_count),
)
.unwrap();
}
fn write_jcode_session(base: &Path) {
let sessions_dir = base.join(".jcode/sessions");
fs::create_dir_all(&sessions_dir).unwrap();
fs::write(
sessions_dir.join("session_cli_fixture.json"),
r#"{
"id":"session_cli_fixture",
"provider_key":"cliproxyapi",
"model":"jcode-cli-model",
"working_dir":"/work/cli-fixture",
"messages":[
{"id":"assistant_1","role":"assistant","timestamp":"2026-01-01T00:00:01Z","token_usage":{"input_tokens":1000,"output_tokens":250,"cache_read_input_tokens":400,"cache_creation_input_tokens":50,"reasoning_output_tokens":25}}
]
}"#,
)
.unwrap();
}
fn write_cursor_usage_cache(base: &Path) {
let cache_dir = base.join(".config/tokmesh/cursor-cache");
fs::create_dir_all(&cache_dir).unwrap();
fs::write(cache_dir.join("usage.csv"), "Date,Model\n").unwrap();
}
fn write_cursor_credentials(base: &Path) {
let config_dir = base.join(".config/tokmesh");
fs::create_dir_all(&config_dir).unwrap();
fs::write(
config_dir.join("cursor-credentials.json"),
serde_json::json!({
"version": 1,
"activeAccountId": "active-account",
"accounts": {
"active-account": {
"sessionToken": "test-session-token",
"userId": "active-account",
"createdAt": "2026-01-01T00:00:00Z",
"label": "work"
}
}
})
.to_string(),
)
.unwrap();
}
#[test]
fn test_help_command() {
let mut cmd = cargo_bin_cmd!("tokmesh");
cmd.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("AI token usage analytics"));
}
#[test]
fn test_help_short_flag() {
let mut cmd = cargo_bin_cmd!("tokmesh");
cmd.arg("-h")
.assert()
.success()
.stdout(predicate::str::contains("AI token usage analytics"));
}
#[test]
fn test_version_flag() {
let mut cmd = cargo_bin_cmd!("tokmesh");
cmd.arg("--version")
.assert()
.success()
.stdout(predicate::str::contains(format!(
"tokmesh {}",
env!("CARGO_PKG_VERSION")
)));
}
#[test]
fn test_models_command_help() {
let mut cmd = cargo_bin_cmd!("tokmesh");
cmd.arg("models")
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("Show model usage report"));
}
#[test]
fn test_monthly_command_help() {
let mut cmd = cargo_bin_cmd!("tokmesh");
cmd.arg("monthly")
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("Show monthly usage report"));
}
#[test]
fn test_pricing_command_help() {
let mut cmd = cargo_bin_cmd!("tokmesh");
cmd.arg("pricing")
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("Show pricing for a model"));
}
#[test]
fn test_clients_command_help() {
let mut cmd = cargo_bin_cmd!("tokmesh");
cmd.arg("clients")
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("Show local scan locations"));
}
#[test]
fn test_codex_command_help() {
let mut cmd = cargo_bin_cmd!("tokmesh");
cmd.arg("codex")
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains(
"Codex account integration commands",
));
}
#[test]
fn test_codex_activity_command_help() {
let mut cmd = cargo_bin_cmd!("tokmesh");
cmd.args(["codex", "activity", "--help"])
.assert()
.success()
.stdout(predicate::str::contains(
"opt-in Codex account-activity snapshot",
));
}
#[test]
fn test_graph_command_help() {
let mut cmd = cargo_bin_cmd!("tokmesh");
cmd.arg("graph")
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("Export contribution graph data"));
}
#[test]
fn test_import_stdout_is_pure_json() {
let home = TempDir::new().unwrap();
let export_path = write_clawdboard_export_fixture(home.path());
let output = cmd_with_home(home.path())
.args(["import", export_path.to_str().unwrap()])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap_or_else(|e| {
panic!(
"stdout was not pure JSON: {e}\nstdout: {}",
String::from_utf8_lossy(&output.stdout)
)
});
assert!(json["summary"]["totalTokens"].as_i64().unwrap() > 0);
assert!(String::from_utf8_lossy(&output.stderr).contains("Import Usage Data"));
}
#[test]
fn test_import_does_not_leak_local_mcp_servers() {
let home = TempDir::new().unwrap();
write_local_mcp_config(home.path());
let export_path = write_clawdboard_export_fixture(home.path());
let output = cmd_with_home(home.path())
.args(["import", export_path.to_str().unwrap()])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert!(
json.get("mcpServers").is_none() || json["mcpServers"].is_null(),
"import output should not carry mcpServers: {json}"
);
assert!(!String::from_utf8_lossy(&output.stdout).contains("local-only-test-server"));
}
#[test]
fn test_tui_command_help() {
let mut cmd = cargo_bin_cmd!("tokmesh");
cmd.arg("tui")
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("Launch interactive TUI"));
}
#[test]
fn test_headless_command_help() {
let mut cmd = cargo_bin_cmd!("tokmesh");
cmd.arg("headless")
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("Capture subprocess output"));
}
#[test]
fn test_login_command_help() {
let tmp = TempDir::new().unwrap();
let mut cmd = leaderboard_cmd_with_home(tmp.path());
cmd.args(["tokscale", "login"])
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("Login"));
}
#[test]
fn test_logout_command_help() {
let tmp = TempDir::new().unwrap();
let mut cmd = leaderboard_cmd_with_home(tmp.path());
cmd.args(["tokensci", "logout"])
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("Logout"));
}
#[test]
fn test_whoami_command_help() {
let tmp = TempDir::new().unwrap();
let mut cmd = leaderboard_cmd_with_home(tmp.path());
cmd.args(["tokscale", "whoami"])
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("logged-in user"));
}
#[test]
fn test_autosubmit_dispatch_keeps_leaderboards_independent() {
let tmp = create_empty_fixture_dir();
let config_dir = tmp.path().join(".config/tokmesh");
leaderboard_cmd_with_home(tmp.path())
.env("TOKMESH_AUTOSUBMIT_SKIP_SCHEDULER", "1")
.args(["tokensci", "autosubmit", "enable", "--interval", "1h"])
.assert()
.success();
let tokensci = leaderboard_cmd_with_home(tmp.path())
.args(["tokensci", "autosubmit", "status", "--json"])
.output()
.unwrap();
let tokscale = leaderboard_cmd_with_home(tmp.path())
.args(["tokscale", "autosubmit", "status", "--json"])
.output()
.unwrap();
let tokensci: serde_json::Value = serde_json::from_slice(&tokensci.stdout).unwrap();
let tokscale: serde_json::Value = serde_json::from_slice(&tokscale.stdout).unwrap();
assert_eq!(tokensci["enabled"], true);
assert_eq!(tokensci["intervalMinutes"], 60);
assert_eq!(tokscale["enabled"], false);
assert!(config_dir.join("autosubmit/tokensci/tokmesh").exists());
assert!(!config_dir.join("autosubmit/tokscale/tokmesh").exists());
}
#[test]
fn test_invalid_command() {
let mut cmd = cargo_bin_cmd!("tokmesh");
cmd.arg("invalid-command").assert().failure();
}
#[test]
fn test_invalid_subcommand() {
let mut cmd = cargo_bin_cmd!("tokmesh");
cmd.arg("models").arg("invalid-flag").assert().failure();
}
#[test]
fn test_codex_accounts_empty_json() {
let tmp = TempDir::new().expect("failed to create temp home");
let mut cmd = cargo_bin_cmd!("tokmesh");
cmd.env("HOME", tmp.path())
.env_remove("CODEX_HOME")
.args(["codex", "accounts", "--json"])
.assert()
.success()
.stdout(predicate::str::contains(r#""accounts": []"#));
}
#[test]
fn test_pricing_command_missing_model() {
let mut cmd = cargo_bin_cmd!("tokmesh");
cmd.arg("pricing").assert().failure();
}
#[test]
fn test_headless_command_missing_client() {
let mut cmd = cargo_bin_cmd!("tokmesh");
cmd.arg("headless").assert().failure();
}
#[test]
fn test_headless_command_invalid_client() {
let mut cmd = cargo_bin_cmd!("tokmesh");
cmd.arg("headless")
.arg("invalid-client")
.arg("test")
.assert()
.failure();
}
#[test]
fn test_models_with_invalid_date_format() {
let tmp = create_empty_fixture_dir();
cmd_with_home(tmp.path())
.arg("models")
.arg("--light")
.args(["--client", "opencode"])
.arg("--no-spinner")
.arg("--since")
.arg("invalid-date")
.assert()
.success();
}
#[test]
fn test_models_with_invalid_year() {
let tmp = create_empty_fixture_dir();
cmd_with_home(tmp.path())
.arg("models")
.arg("--light")
.args(["--client", "opencode"])
.arg("--no-spinner")
.arg("--year")
.arg("not-a-year")
.assert()
.success();
}
#[test]
fn test_global_theme_flag() {
let mut cmd = cargo_bin_cmd!("tokmesh");
cmd.arg("--theme")
.arg("blue")
.arg("--help")
.assert()
.success();
}
#[test]
fn test_global_debug_flag() {
let mut cmd = cargo_bin_cmd!("tokmesh");
cmd.arg("--debug").arg("--help").assert().success();
}
#[test]
fn test_models_with_since_until_filter() {
let tmp = create_temp_fixture_dir();
cmd_with_home(tmp.path())
.args(["models", "--json", "--client", "opencode", "--no-spinner"])
.args(["--since", "2024-06-01", "--until", "2024-06-30"])
.assert()
.success()
.stdout(predicate::str::contains("claude-sonnet-4"))
.stdout(predicate::str::contains("gpt-4o").not());
}
#[test]
fn test_models_with_year_filter() {
let tmp = create_temp_fixture_dir();
cmd_with_home(tmp.path())
.args(["models", "--json", "--client", "opencode", "--no-spinner"])
.args(["--year", "2024"])
.assert()
.success()
.stdout(predicate::str::contains("claude-sonnet-4"))
.stdout(predicate::str::contains("gpt-4o").not());
}
#[test]
fn test_monthly_with_date_filters() {
let tmp = create_temp_fixture_dir();
cmd_with_home(tmp.path())
.args(["monthly", "--json", "--client", "opencode", "--no-spinner"])
.args(["--since", "2025-01-01", "--until", "2025-12-31"])
.assert()
.success()
.stdout(predicate::str::contains("2025-01"));
}
#[test]
fn test_models_home_override_ignores_conflicting_xdg_env() {
let real_home = create_temp_fixture_dir();
let conflicting_home = create_conflicting_opencode_fixture_dir();
let output = cmd_with_conflicting_env(conflicting_home.path())
.args([
"models",
"--json",
"--client",
"opencode",
"--no-spinner",
"--home",
real_home.path().to_str().unwrap(),
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(json["totalMessages"].as_i64().unwrap(), 3);
assert_eq!(json["totalInput"].as_i64().unwrap(), 2400);
assert_eq!(json["totalOutput"].as_i64().unwrap(), 1000);
assert!(!String::from_utf8_lossy(&output.stdout).contains("gemini-2.5-pro"));
}
#[test]
fn test_monthly_home_override_ignores_conflicting_xdg_env() {
let real_home = create_temp_fixture_dir();
let conflicting_home = create_conflicting_opencode_fixture_dir();
let output = cmd_with_conflicting_env(conflicting_home.path())
.args([
"monthly",
"--json",
"--client",
"opencode",
"--no-spinner",
"--home",
real_home.path().to_str().unwrap(),
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let entries = json["entries"].as_array().unwrap();
assert_eq!(entries.len(), 2);
assert!(entries.iter().any(|entry| entry["month"] == "2024-06"));
assert!(entries.iter().any(|entry| entry["month"] == "2025-01"));
assert!(!String::from_utf8_lossy(&output.stdout).contains("gemini-2.5-pro"));
}
#[test]
fn test_graph_home_override_ignores_conflicting_xdg_env() {
let real_home = create_temp_fixture_dir();
let conflicting_home = create_conflicting_opencode_fixture_dir();
let output = cmd_with_conflicting_env(conflicting_home.path())
.args([
"graph",
"--client",
"opencode",
"--no-spinner",
"--home",
real_home.path().to_str().unwrap(),
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let contributions = json["contributions"].as_array().unwrap();
assert_eq!(contributions.len(), 2);
assert!(!String::from_utf8_lossy(&output.stdout).contains("gemini-2.5-pro"));
}
#[test]
fn test_models_home_override_ignores_conflicting_codex_home_env() {
let real_home = create_codex_fixture_dir();
let conflicting_home = create_conflicting_codex_fixture_dir();
let output = cmd_with_conflicting_env(conflicting_home.path())
.env("CODEX_HOME", conflicting_home.path().join(".codex"))
.args([
"models",
"--json",
"--client",
"codex",
"--no-spinner",
"--home",
real_home.path().to_str().unwrap(),
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(json["totalMessages"].as_i64().unwrap(), 1);
assert_eq!(json["totalInput"].as_i64().unwrap(), 100);
assert_eq!(json["totalOutput"].as_i64().unwrap(), 30);
assert_eq!(json["totalCacheRead"].as_i64().unwrap(), 20);
assert!(!String::from_utf8_lossy(&output.stdout).contains("\"gpt-5\""));
}
#[test]
fn test_tui_rejects_home_override() {
let tmp = TempDir::new().unwrap();
cargo_bin_cmd!("tokmesh")
.args(["--home", tmp.path().to_str().unwrap(), "tui"])
.assert()
.failure()
.stderr(predicate::str::contains(
"--home is currently supported for local report commands only",
));
}
#[test]
fn test_clients_home_override_uses_explicit_home_for_json() {
let real_home = create_codex_fixture_dir();
let conflicting_home = create_conflicting_codex_fixture_dir();
write_codex_token_session(
&real_home.path().join(".codex/sessions"),
"session-2.jsonl",
"gpt-4o-mini",
80,
20,
);
let output = cmd_with_conflicting_env(conflicting_home.path())
.env("CODEX_HOME", conflicting_home.path().join(".codex"))
.args([
"--home",
real_home.path().to_str().unwrap(),
"clients",
"--json",
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let codex = json["clients"]
.as_array()
.unwrap()
.iter()
.find(|row| row["client"] == "codex")
.unwrap();
assert_eq!(
codex["sessionsPath"],
serde_json::json!(real_home.path().join(".codex/sessions"))
);
assert_eq!(codex["messageCount"].as_i64().unwrap(), 2);
}
#[test]
fn test_clients_home_override_ignores_copilot_exporter_env() {
let real_home = create_empty_fixture_dir();
let conflicting_home = create_empty_fixture_dir();
let exporter_file = conflicting_home.path().join("copilot-host.jsonl");
fs::write(&exporter_file, "{}").unwrap();
let output = cmd_with_conflicting_env(conflicting_home.path())
.env("COPILOT_OTEL_FILE_EXPORTER_PATH", &exporter_file)
.args([
"--home",
real_home.path().to_str().unwrap(),
"clients",
"--json",
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let copilot = json["clients"]
.as_array()
.unwrap()
.iter()
.find(|row| row["client"] == "copilot")
.unwrap();
assert!(
copilot.get("exporterStatus").is_none(),
"explicit --home diagnostics must not report host COPILOT_OTEL_FILE_EXPORTER_PATH: {copilot:#?}"
);
}
#[test]
fn test_models_with_since_only() {
let tmp = create_temp_fixture_dir();
cmd_with_home(tmp.path())
.args(["models", "--json", "--client", "opencode", "--no-spinner"])
.args(["--since", "2025-01-01"])
.assert()
.success()
.stdout(predicate::str::contains("gpt-4o"))
.stdout(predicate::str::contains("anthropic").not());
}
#[test]
fn test_models_with_until_only() {
let tmp = create_temp_fixture_dir();
cmd_with_home(tmp.path())
.args(["models", "--json", "--client", "opencode", "--no-spinner"])
.args(["--until", "2024-12-31"])
.assert()
.success()
.stdout(predicate::str::contains("claude-sonnet-4"))
.stdout(predicate::str::contains("gpt-4o").not());
}
#[test]
fn test_models_with_no_matching_date() {
let tmp = create_temp_fixture_dir();
let output = cmd_with_home(tmp.path())
.args(["models", "--json", "--client", "opencode", "--no-spinner"])
.args(["--since", "2099-01-01", "--until", "2099-12-31"])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let entries = json["entries"].as_array().unwrap();
assert!(
entries.is_empty(),
"No entries expected for future date range"
);
}
#[test]
fn test_graph_single_day_filter_uses_local_timezone_boundaries() {
let tmp = create_timezone_boundary_fixture_dir();
write_settings_json(tmp.path(), r#"{"bucketTimezone":"America/Los_Angeles"}"#);
let output = cmd_with_home(tmp.path())
.args(["graph", "--client", "opencode", "--no-spinner"])
.args(["--since", "2026-03-02", "--until", "2026-03-02"])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let contributions = json["contributions"].as_array().unwrap();
assert_eq!(
contributions.len(),
1,
"expected a single local-day bucket, got {:?}",
contributions
);
assert_eq!(contributions[0]["date"].as_str().unwrap(), "2026-03-02");
assert_eq!(contributions[0]["totals"]["messages"].as_i64().unwrap(), 2);
}
#[test]
fn test_graph_opencodereview_uses_unified_parse_path() {
let tmp = create_opencodereview_fixture_dir();
let output = cmd_with_home(tmp.path())
.args(["graph", "--client", "opencodereview", "--no-spinner"])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let contribution = &json["contributions"][0];
assert_eq!(contribution["totals"]["tokens"].as_i64(), Some(1400));
assert_eq!(contribution["totals"]["messages"].as_i64(), Some(1));
assert_eq!(
contribution["clients"][0]["client"].as_str(),
Some("opencodereview")
);
assert_eq!(
contribution["clients"][0]["modelId"].as_str(),
Some("gpt-4o")
);
}
#[test]
fn test_graph_with_year_filter() {
let tmp = create_temp_fixture_dir();
let output = cmd_with_home(tmp.path())
.args(["graph", "--client", "opencode", "--no-spinner"])
.args(["--year", "2024"])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let contributions = json["contributions"].as_array().unwrap();
for c in contributions {
let date = c["date"].as_str().unwrap();
assert!(
date.starts_with("2024-"),
"Expected 2024 dates, got {}",
date
);
}
}
#[test]
fn test_models_with_client_filter_opencode() {
let tmp = create_temp_fixture_dir();
let output = cmd_with_home(tmp.path())
.args(["models", "--json", "--client", "opencode", "--no-spinner"])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let entries = json["entries"].as_array().unwrap();
for entry in entries {
assert_eq!(entry["client"].as_str().unwrap(), "opencode");
}
}
#[test]
fn test_models_with_client_filter_multiple() {
let tmp = create_temp_fixture_dir();
cmd_with_home(tmp.path())
.args([
"models",
"--json",
"--client",
"opencode",
"--client",
"claude",
"--no-spinner",
])
.assert()
.success();
}
#[test]
fn test_models_with_client_filter_jcode() {
let tmp = create_empty_fixture_dir();
write_jcode_session(tmp.path());
let output = cmd_with_home(tmp.path())
.args(["models", "--json", "--client", "jcode", "--no-spinner"])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let entries = json["entries"].as_array().unwrap();
assert_eq!(entries.len(), 1);
let entry = &entries[0];
assert_eq!(entry["client"].as_str().unwrap(), "jcode");
assert_eq!(entry["provider"].as_str().unwrap(), "cliproxyapi");
assert_eq!(entry["model"].as_str().unwrap(), "jcode-cli-model");
assert_eq!(entry["input"].as_i64().unwrap(), 1000);
assert_eq!(entry["cacheRead"].as_i64().unwrap(), 400);
assert_eq!(entry["cacheWrite"].as_i64().unwrap(), 50);
assert_eq!(entry["output"].as_i64().unwrap(), 250);
assert_eq!(entry["reasoning"].as_i64().unwrap(), 25);
}
fn assert_cursor_setup_warning(json: &serde_json::Value) {
let warnings = json["warnings"]
.as_array()
.expect("explicit Cursor report should expose setup warnings");
assert!(
warnings
.iter()
.any(|warning| warning.as_str().is_some_and(|text| text
.contains("tokmesh cursor login")
&& text.contains("tokmesh cursor sync --json")
&& text.contains("cursor-cache/usage*.csv")
&& text.contains("Tokmesh does not parse local `~/.cursor`"))),
"warnings did not explain Cursor setup: {warnings:?}"
);
}
#[test]
fn test_models_cursor_explicit_missing_cache_reports_setup_warning_json() {
let tmp = create_empty_fixture_dir();
let output = cmd_with_home(tmp.path())
.args(["models", "--json", "--client", "cursor", "--no-spinner"])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_cursor_setup_warning(&json);
}
#[test]
fn test_models_cursor_explicit_local_cursor_state_still_reports_setup_warning_json() {
let tmp = create_empty_fixture_dir();
fs::create_dir_all(
tmp.path()
.join(".cursor/projects/demo/agent-transcripts/session"),
)
.unwrap();
fs::write(
tmp.path()
.join(".cursor/projects/demo/agent-transcripts/session/session.jsonl"),
r#"{"role":"user","content":"hello"}"#,
)
.unwrap();
let output = cmd_with_home(tmp.path())
.args(["models", "--json", "--client", "cursor", "--no-spinner"])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_cursor_setup_warning(&json);
}
#[test]
fn test_monthly_cursor_explicit_missing_cache_reports_setup_warning_json() {
let tmp = create_empty_fixture_dir();
let output = cmd_with_home(tmp.path())
.args(["monthly", "--json", "--client", "cursor", "--no-spinner"])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_cursor_setup_warning(&json);
}
#[test]
fn test_hourly_cursor_explicit_missing_cache_reports_setup_warning_json() {
let tmp = create_empty_fixture_dir();
let output = cmd_with_home(tmp.path())
.args(["hourly", "--json", "--client", "cursor", "--no-spinner"])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_cursor_setup_warning(&json);
}
#[test]
fn test_models_cursor_explicit_home_override_reports_fixture_cache_path() {
let tmp = create_empty_fixture_dir();
let output = cmd_with_home(tmp.path())
.args([
"--home",
tmp.path().to_str().unwrap(),
"models",
"--json",
"--client",
"cursor",
"--no-spinner",
])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let warnings = json["warnings"]
.as_array()
.expect("explicit Cursor --home report should expose setup warnings");
assert!(
warnings
.iter()
.any(|warning| warning.as_str().is_some_and(|text| text
.contains(tmp.path().to_str().unwrap())
&& text.contains("tokmesh cursor login")
&& text.contains("tokmesh cursor sync --json")
&& text.contains("cursor-cache/usage*.csv"))),
"warnings did not explain Cursor --home setup: {warnings:?}"
);
}
#[test]
fn test_models_cursor_explicit_missing_cache_reports_setup_warning_text() {
let tmp = create_empty_fixture_dir();
cmd_with_home(tmp.path())
.args(["models", "--client", "cursor", "--no-spinner"])
.assert()
.success()
.stderr(predicate::str::contains("Cursor usage requires"))
.stderr(predicate::str::contains("tokmesh cursor login"))
.stderr(predicate::str::contains("tokmesh cursor sync --json"))
.stderr(predicate::str::contains(
"Tokmesh does not parse local `~/.cursor`",
));
}
#[test]
fn test_models_default_missing_cursor_cache_does_not_emit_setup_warning_json() {
let tmp = create_empty_fixture_dir();
let output = cmd_with_home(tmp.path())
.args(["models", "--json", "--no-spinner"])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert!(
json.get("warnings")
.and_then(serde_json::Value::as_array)
.is_none_or(Vec::is_empty),
"default all-client report should not warn about unrequested Cursor setup"
);
}
#[test]
fn test_models_cursor_explicit_existing_cache_suppresses_setup_warning_json() {
let tmp = create_empty_fixture_dir();
write_cursor_usage_cache(tmp.path());
let output = cmd_with_home(tmp.path())
.args(["models", "--json", "--client", "cursor", "--no-spinner"])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert!(
json.get("warnings")
.and_then(serde_json::Value::as_array)
.is_none_or(Vec::is_empty),
"existing Cursor cache should suppress setup warnings"
);
}
#[test]
fn test_models_cursor_logged_in_missing_cache_suggests_sync_only_json() {
let tmp = create_empty_fixture_dir();
write_cursor_credentials(tmp.path());
let output = cmd_with_home(tmp.path())
.env("HTTPS_PROXY", "http://127.0.0.1:9")
.env("HTTP_PROXY", "http://127.0.0.1:9")
.env("ALL_PROXY", "http://127.0.0.1:9")
.args(["models", "--json", "--client", "cursor", "--no-spinner"])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let warnings = json["warnings"].as_array().unwrap();
let warning = warnings[0].as_str().unwrap();
assert!(warning.contains("tokmesh cursor sync --json"));
assert!(
!warning.contains("tokmesh cursor login"),
"logged-in users with no cache should be told to sync, not log in again: {warning}"
);
}
#[test]
fn test_time_metrics_cursor_explicit_missing_cache_reports_setup_warning_json() {
let tmp = create_empty_fixture_dir();
let output = cmd_with_home(tmp.path())
.args([
"time-metrics",
"--json",
"--client",
"cursor",
"--no-spinner",
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_cursor_setup_warning(&json);
}
#[test]
fn test_graph_cursor_explicit_missing_cache_reports_setup_warning_text() {
let tmp = create_empty_fixture_dir();
cmd_with_home(tmp.path())
.args(["graph", "--client", "cursor", "--no-spinner"])
.assert()
.success()
.stderr(predicate::str::contains("Cursor usage requires"))
.stderr(predicate::str::contains("tokmesh cursor login"));
}
#[test]
fn test_graph_fresh_cursor_cache_skips_auto_sync_warning() {
let tmp = create_empty_fixture_dir();
write_cursor_credentials(tmp.path());
write_cursor_usage_cache(tmp.path());
let output = cmd_with_home(tmp.path())
.env("HTTPS_PROXY", "http://127.0.0.1:9")
.env("HTTP_PROXY", "http://127.0.0.1:9")
.env("ALL_PROXY", "http://127.0.0.1:9")
.args(["graph", "--client", "cursor", "--no-spinner"])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stderr.contains("Cursor sync failed") && !stderr.contains("Cursor sync warning"),
"fresh Cursor cache should skip implicit graph sync; stderr: {stderr}"
);
}
#[test]
fn test_submit_cursor_explicit_missing_cache_reports_setup_warning_text() {
let tmp = create_empty_fixture_dir();
leaderboard_cmd_with_home(tmp.path())
.env("TOKMESH_TOKSCALE_API_TOKEN", "test-token")
.args(["tokscale", "submit", "--client", "cursor", "--dry-run"])
.assert()
.success()
.stderr(predicate::str::contains("Cursor usage requires"))
.stderr(predicate::str::contains("tokmesh cursor login"));
}
#[test]
fn test_submit_dry_run_preserves_local_date_ahead_of_utc() {
let (tmp, expected_local_date) = create_positive_utc_offset_submit_fixture_dir();
write_settings_json(tmp.path(), r#"{"bucketTimezone":"Pacific/Kiritimati"}"#);
leaderboard_cmd_with_home(tmp.path())
.env("TOKMESH_TOKSCALE_API_TOKEN", "test-token")
.args(["tokscale", "submit", "--client", "opencode", "--dry-run"])
.assert()
.success()
.stdout(predicate::str::contains(format!(
"Date range: {expected_local_date} to {expected_local_date}"
)))
.stdout(predicate::str::contains("Total tokens: 1,750"));
}
#[test]
fn test_submit_dry_run_json_is_pure_json_and_needs_no_token() {
let tmp = create_temp_fixture_dir();
let output = leaderboard_cmd_with_home(tmp.path())
.args([
"tokensci",
"submit",
"--client",
"opencode",
"--dry-run",
"--json",
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let payload: serde_json::Value =
serde_json::from_slice(&output.stdout).unwrap_or_else(|error| {
panic!(
"stdout was not pure JSON: {error}; stdout={}",
String::from_utf8_lossy(&output.stdout)
)
});
assert!(payload.get("summary").is_some());
assert!(payload.get("clientManifest").is_some());
assert!(tmp.path().join(".config/tokmesh/device.json").exists());
}
#[test]
fn test_submit_replace_validates_filtered_client_contributions() {
let tmp = create_temp_fixture_dir();
leaderboard_cmd_with_home(tmp.path())
.args([
"tokensci",
"submit",
"--client",
"opencode,codex",
"--since",
"2024-01-01",
"--until",
"2025-12-31",
"--replace",
"--dry-run",
"--json",
])
.assert()
.failure()
.stderr(predicate::str::contains("no contribution found for: codex"));
}
#[test]
fn test_models_with_all_client_flags() {
let tmp = create_temp_fixture_dir();
cmd_with_home(tmp.path())
.args([
"models",
"--json",
"--no-spinner",
"--client",
"opencode",
"--client",
"claude",
"--client",
"codex",
"--client",
"gemini",
"--client",
"cursor",
"--client",
"amp",
"--client",
"droid",
"--client",
"openclaw",
"--client",
"pi",
])
.assert()
.success();
}
#[test]
fn test_models_client_and_date_combined() {
let tmp = create_temp_fixture_dir();
cmd_with_home(tmp.path())
.args(["models", "--json", "--client", "opencode", "--no-spinner"])
.args(["--year", "2025"])
.assert()
.success()
.stdout(predicate::str::contains("gpt-4o"))
.stdout(predicate::str::contains("anthropic").not());
}
#[test]
fn test_models_json_output() {
let tmp = create_temp_fixture_dir();
let output = cmd_with_home(tmp.path())
.args(["models", "--json", "--client", "opencode", "--no-spinner"])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert!(json.get("groupBy").is_some(), "Missing groupBy field");
assert!(json.get("entries").is_some(), "Missing entries field");
assert!(json.get("totalInput").is_some(), "Missing totalInput");
assert!(json.get("totalOutput").is_some(), "Missing totalOutput");
assert!(
json.get("totalCacheRead").is_some(),
"Missing totalCacheRead"
);
assert!(
json.get("totalCacheWrite").is_some(),
"Missing totalCacheWrite"
);
assert!(json.get("totalMessages").is_some(), "Missing totalMessages");
assert!(json.get("totalCost").is_some(), "Missing totalCost");
assert!(
json.get("processingTimeMs").is_some(),
"Missing processingTimeMs"
);
let entries = json["entries"].as_array().unwrap();
assert!(!entries.is_empty(), "Should have entries from fixture data");
let first = &entries[0];
assert!(first.get("client").is_some());
assert!(first.get("model").is_some());
assert!(first.get("provider").is_some());
assert!(first.get("input").is_some());
assert!(first.get("output").is_some());
assert!(first.get("cacheRead").is_some());
assert!(first.get("cacheWrite").is_some());
assert!(first.get("cost").is_some());
let performance = first
.get("performance")
.expect("Missing performance")
.as_object()
.expect("performance should be an object");
assert!(performance.contains_key("msPer1KTokens"));
assert!(performance.contains_key("totalDurationMs"));
assert!(performance.contains_key("timedTokens"));
assert!(performance.contains_key("sampleCount"));
assert!(performance.contains_key("tokenCoverage"));
assert!(performance["msPer1KTokens"].as_f64().unwrap() > 0.0);
}
#[test]
fn test_models_json_offline_without_pricing_cache_still_succeeds() {
let tmp = create_temp_fixture_dir_without_pricing_cache();
let output = offline_cmd_with_home(tmp.path())
.args(["models", "--json", "--client", "opencode", "--no-spinner"])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(json["totalInput"].as_i64().unwrap(), 2400);
assert_eq!(json["totalOutput"].as_i64().unwrap(), 1000);
assert_eq!(json["totalMessages"].as_i64().unwrap(), 3);
assert_eq!(json["entries"].as_array().unwrap().len(), 2);
let total_cost = json["totalCost"].as_f64().unwrap();
assert!(
(total_cost - 0.10).abs() < 1e-9,
"unexpected totalCost without pricing: {total_cost}"
);
}
#[test]
fn test_monthly_json_offline_without_pricing_cache_still_succeeds() {
let tmp = create_temp_fixture_dir_without_pricing_cache();
let output = offline_cmd_with_home(tmp.path())
.args(["monthly", "--json", "--client", "opencode", "--no-spinner"])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let entries = json["entries"].as_array().unwrap();
assert_eq!(entries.len(), 2);
assert_eq!(entries[0]["month"].as_str().unwrap(), "2024-06");
assert_eq!(entries[1]["month"].as_str().unwrap(), "2025-01");
let total_cost = json["totalCost"].as_f64().unwrap();
assert!(
(total_cost - 0.10).abs() < 1e-9,
"unexpected totalCost without pricing: {total_cost}"
);
}
#[test]
fn test_graph_offline_without_pricing_cache_still_succeeds() {
let tmp = create_temp_fixture_dir_without_pricing_cache();
let output = offline_cmd_with_home(tmp.path())
.args(["graph", "--client", "opencode", "--no-spinner"])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(json["summary"]["totalTokens"].as_i64().unwrap(), 3950);
assert_eq!(json["summary"]["activeDays"].as_i64().unwrap(), 2);
assert_eq!(json["contributions"].as_array().unwrap().len(), 2);
let total_cost = json["summary"]["totalCost"].as_f64().unwrap();
assert!(
(total_cost - 0.10).abs() < 1e-9,
"unexpected totalCost without pricing: {total_cost}"
);
}
#[test]
fn test_hourly_json_offline_without_pricing_cache_still_succeeds() {
let tmp = create_temp_fixture_dir_without_pricing_cache();
let output = offline_cmd_with_home(tmp.path())
.args(["hourly", "--json", "--client", "opencode", "--no-spinner"])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let entries = json["entries"].as_array().unwrap();
assert_eq!(entries.len(), 3);
assert_eq!(
entries
.iter()
.map(|entry| entry["input"].as_i64().unwrap())
.sum::<i64>(),
2400
);
assert_eq!(
entries
.iter()
.map(|entry| entry["output"].as_i64().unwrap())
.sum::<i64>(),
1000
);
let total_cost = json["totalCost"].as_f64().unwrap();
assert!(
(total_cost - 0.10).abs() < 1e-9,
"unexpected totalCost without pricing: {total_cost}"
);
}
#[test]
fn test_models_json_offline_uses_stale_pricing_cache_when_available() {
let tmp = create_temp_fixture_dir_without_pricing_cache();
write_pricing_cache(tmp.path(), 1);
add_uncosted_opencode_message(tmp.path());
let output = offline_cmd_with_home(tmp.path())
.args(["models", "--json", "--client", "opencode", "--no-spinner"])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let total_cost = json["totalCost"].as_f64().unwrap();
assert!(
(total_cost - 0.1065).abs() < 1e-9,
"unexpected totalCost: {total_cost}"
);
}
#[test]
fn test_monthly_json_offline_uses_stale_pricing_cache_when_available() {
let tmp = create_temp_fixture_dir_without_pricing_cache();
write_pricing_cache(tmp.path(), 1);
add_uncosted_opencode_message(tmp.path());
let output = offline_cmd_with_home(tmp.path())
.args(["monthly", "--json", "--client", "opencode", "--no-spinner"])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let total_cost = json["totalCost"].as_f64().unwrap();
assert!(
(total_cost - 0.1065).abs() < 1e-9,
"unexpected totalCost: {total_cost}"
);
}
#[test]
fn test_graph_offline_uses_stale_pricing_cache_when_available() {
let tmp = create_temp_fixture_dir_without_pricing_cache();
write_pricing_cache(tmp.path(), 1);
add_uncosted_opencode_message(tmp.path());
let output = offline_cmd_with_home(tmp.path())
.args(["graph", "--client", "opencode", "--no-spinner"])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let total_cost = json["summary"]["totalCost"].as_f64().unwrap();
assert!(
(total_cost - 0.1065).abs() < 1e-9,
"unexpected totalCost: {total_cost}"
);
}
#[test]
fn test_hourly_json_offline_uses_stale_pricing_cache_when_available() {
let tmp = create_temp_fixture_dir_without_pricing_cache();
write_pricing_cache(tmp.path(), 1);
add_uncosted_opencode_message(tmp.path());
let output = offline_cmd_with_home(tmp.path())
.args(["hourly", "--json", "--client", "opencode", "--no-spinner"])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let entries = json["entries"].as_array().unwrap();
assert_eq!(entries.len(), 3);
assert_eq!(
entries
.iter()
.map(|entry| entry["input"].as_i64().unwrap())
.sum::<i64>(),
3400
);
assert_eq!(
entries
.iter()
.map(|entry| entry["output"].as_i64().unwrap())
.sum::<i64>(),
1400
);
let total_cost = json["totalCost"].as_f64().unwrap();
assert!(
(total_cost - 0.1065).abs() < 1e-9,
"unexpected totalCost: {total_cost}"
);
}
#[test]
fn test_empty_report_total_cost_is_positive_zero() {
let tmp = TempDir::new().unwrap();
for subcmd in ["models", "monthly", "hourly"] {
let output = offline_cmd_with_home(tmp.path())
.args([subcmd, "--json", "--client", "crush", "--no-spinner"])
.output()
.unwrap();
assert!(
output.status.success(),
"{subcmd} stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
!stdout.contains("-0.0"),
"{subcmd} JSON contains negative zero: {stdout}"
);
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let total_cost = json["totalCost"].as_f64().unwrap();
assert_eq!(total_cost, 0.0, "{subcmd} totalCost should be zero");
assert!(
total_cost.is_sign_positive(),
"{subcmd} totalCost serialized as -0.0"
);
}
}
#[test]
fn test_hide_zero_drops_all_zero_entries_but_keeps_totals() {
let tmp = create_temp_fixture_dir_without_pricing_cache();
add_zero_usage_opencode_message(tmp.path());
let run = |args: &[&str]| -> serde_json::Value {
let output = offline_cmd_with_home(tmp.path())
.args(args)
.args(["--json", "--client", "opencode", "--no-spinner"])
.output()
.unwrap();
assert!(
output.status.success(),
"{args:?} stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
serde_json::from_slice(&output.stdout).unwrap()
};
let full = run(&["models"]);
let filtered = run(&["models", "--hide-zero"]);
let has_zero_model = |json: &serde_json::Value| {
json["entries"]
.as_array()
.unwrap()
.iter()
.any(|e| e["model"] == "zero-model")
};
assert!(has_zero_model(&full), "zero row must show without the flag");
assert!(!has_zero_model(&filtered), "--hide-zero must drop the row");
assert!(filtered["entries"].as_array().unwrap().iter().all(|e| {
e["input"].as_i64().unwrap() != 0
|| e["output"].as_i64().unwrap() != 0
|| e["cost"].as_f64().unwrap() != 0.0
}));
assert_eq!(full["totalMessages"], filtered["totalMessages"]);
assert_eq!(full["totalCost"], filtered["totalCost"]);
let full = run(&["monthly"]);
let filtered = run(&["monthly", "--hide-zero"]);
let months = |json: &serde_json::Value| -> Vec<String> {
json["entries"]
.as_array()
.unwrap()
.iter()
.map(|e| e["month"].as_str().unwrap().to_string())
.collect()
};
assert!(months(&full).contains(&"2023-03".to_string()));
assert!(!months(&filtered).contains(&"2023-03".to_string()));
assert_eq!(full["totalCost"], filtered["totalCost"]);
let full = run(&["hourly"]);
let filtered = run(&["hourly", "--hide-zero"]);
assert_eq!(
full["entries"].as_array().unwrap().len(),
filtered["entries"].as_array().unwrap().len() + 1
);
assert_eq!(full["totalCost"], filtered["totalCost"]);
}
#[test]
fn test_models_json_total_consistency() {
let tmp = create_temp_fixture_dir();
let output = cmd_with_home(tmp.path())
.args(["models", "--json", "--client", "opencode", "--no-spinner"])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let entries = json["entries"].as_array().unwrap();
let sum_input: i64 = entries.iter().map(|e| e["input"].as_i64().unwrap()).sum();
let sum_output: i64 = entries.iter().map(|e| e["output"].as_i64().unwrap()).sum();
let total_input = json["totalInput"].as_i64().unwrap();
let total_output = json["totalOutput"].as_i64().unwrap();
assert_eq!(
sum_input, total_input,
"Sum of entry inputs must match totalInput"
);
assert_eq!(
sum_output, total_output,
"Sum of entry outputs must match totalOutput"
);
}
#[test]
fn test_monthly_json_output() {
let tmp = create_temp_fixture_dir();
let output = cmd_with_home(tmp.path())
.args(["monthly", "--json", "--client", "opencode", "--no-spinner"])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert!(json.get("entries").is_some(), "Missing entries field");
assert!(json.get("totalCost").is_some(), "Missing totalCost field");
assert!(
json.get("processingTimeMs").is_some(),
"Missing processingTimeMs"
);
let entries = json["entries"].as_array().unwrap();
assert!(
!entries.is_empty(),
"Should have monthly entries from fixture data"
);
let first = &entries[0];
assert!(first.get("month").is_some());
assert!(first.get("models").is_some());
assert!(first.get("input").is_some());
assert!(first.get("output").is_some());
assert!(first.get("cacheRead").is_some());
assert!(first.get("cacheWrite").is_some());
assert!(first.get("messageCount").is_some());
assert!(first.get("cost").is_some());
}
#[test]
fn test_hourly_home_override_uses_explicit_home_scanner_settings() {
let real_home = create_empty_fixture_dir();
let conflicting_home = create_conflicting_codex_fixture_dir();
let extra_home = TempDir::new().unwrap();
let extra_sessions = extra_home.path().join("portable-codex/sessions");
write_codex_token_session(
&extra_sessions,
"settings-session.jsonl",
"gpt-4o-mini",
210,
40,
);
write_settings_json(
real_home.path(),
&format!(
r#"{{
"scanner": {{
"extraScanPaths": {{
"codex": [{}]
}}
}}
}}"#,
serde_json::to_string(extra_sessions.to_str().unwrap()).unwrap()
),
);
let output = cmd_with_conflicting_env(conflicting_home.path())
.env("TOKMESH_PRICING_CACHE_ONLY", "1")
.env("CODEX_HOME", conflicting_home.path().join(".codex"))
.args([
"hourly",
"--json",
"--client",
"codex",
"--no-spinner",
"--home",
real_home.path().to_str().unwrap(),
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(json["entries"].as_array().unwrap().len(), 1);
assert_eq!(json["entries"][0]["input"].as_i64().unwrap(), 210);
assert_eq!(json["entries"][0]["output"].as_i64().unwrap(), 40);
assert!(!String::from_utf8_lossy(&output.stdout).contains("gpt-5"));
}
#[test]
fn test_monthly_json_with_client_filter() {
let tmp = create_temp_fixture_dir();
let output = cmd_with_home(tmp.path())
.args(["monthly", "--json", "--client", "opencode", "--no-spinner"])
.args(["--year", "2024"])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let entries = json["entries"].as_array().unwrap();
for entry in entries {
let month = entry["month"].as_str().unwrap();
assert!(
month.starts_with("2024-"),
"Expected 2024 months only, got {}",
month
);
}
}
#[test]
fn test_graph_json_output() {
let tmp = create_temp_fixture_dir();
let output = cmd_with_home(tmp.path())
.args(["graph", "--client", "opencode", "--no-spinner"])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert!(json.get("meta").is_some(), "Missing meta field");
assert!(json.get("summary").is_some(), "Missing summary field");
assert!(json.get("years").is_some(), "Missing years field");
assert!(
json.get("contributions").is_some(),
"Missing contributions field"
);
}
#[test]
fn test_graph_json_has_meta() {
let tmp = create_temp_fixture_dir();
let output = cmd_with_home(tmp.path())
.args(["graph", "--client", "opencode", "--no-spinner"])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let meta = &json["meta"];
assert!(
meta.get("generatedAt").is_some(),
"Missing meta.generatedAt"
);
assert!(meta.get("version").is_some(), "Missing meta.version");
assert!(meta.get("dateRange").is_some(), "Missing meta.dateRange");
}
#[test]
fn test_graph_json_has_summary() {
let tmp = create_temp_fixture_dir();
let output = cmd_with_home(tmp.path())
.args(["graph", "--client", "opencode", "--no-spinner"])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let summary = &json["summary"];
assert!(
summary.get("totalTokens").is_some(),
"Missing summary.totalTokens"
);
assert!(
summary.get("totalCost").is_some(),
"Missing summary.totalCost"
);
assert!(
summary.get("totalDays").is_some(),
"Missing summary.totalDays"
);
assert!(
summary.get("activeDays").is_some(),
"Missing summary.activeDays"
);
assert!(summary.get("clients").is_some(), "Missing summary.clients");
assert!(summary.get("models").is_some(), "Missing summary.models");
}
#[test]
fn test_models_group_by_default() {
let tmp = create_temp_fixture_dir();
let output = cmd_with_home(tmp.path())
.args(["models", "--json", "--client", "opencode", "--no-spinner"])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(json["groupBy"].as_str().unwrap(), "client,model");
}
#[test]
fn test_models_group_by_model() {
let tmp = create_temp_fixture_dir();
let output = cmd_with_home(tmp.path())
.args(["models", "--json", "--client", "opencode", "--no-spinner"])
.args(["--group-by", "model"])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(json["groupBy"].as_str().unwrap(), "model");
let entries = json["entries"].as_array().unwrap();
let models: Vec<&str> = entries
.iter()
.map(|e| e["model"].as_str().unwrap())
.collect();
let unique_models: std::collections::HashSet<&&str> = models.iter().collect();
assert_eq!(
models.len(),
unique_models.len(),
"group-by model should produce unique model entries"
);
}
#[test]
fn test_models_group_by_client_provider_model() {
let tmp = create_temp_fixture_dir();
let output = cmd_with_home(tmp.path())
.args(["models", "--json", "--client", "opencode", "--no-spinner"])
.args(["--group-by", "client,provider,model"])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(json["groupBy"].as_str().unwrap(), "client,provider,model");
let entries = json["entries"].as_array().unwrap();
for entry in entries {
assert!(entry.get("client").is_some(), "Entry must have client");
assert!(entry.get("provider").is_some(), "Entry must have provider");
assert!(entry.get("model").is_some(), "Entry must have model");
}
}
#[test]
fn test_models_json_with_group_by_model() {
let tmp = create_temp_fixture_dir();
let output = cmd_with_home(tmp.path())
.args(["models", "--json", "--client", "opencode", "--no-spinner"])
.args(["--group-by", "model"])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let entries = json["entries"].as_array().unwrap();
for entry in entries {
assert!(
entry.get("mergedClients").is_some(),
"group-by model entries should have mergedClients field"
);
assert!(
entry.get("workspaceKey").is_none(),
"group-by model entries should not expose workspaceKey"
);
assert!(
entry.get("workspaceLabel").is_none(),
"group-by model entries should not expose workspaceLabel"
);
assert!(
entry.get("sessionId").is_none(),
"group-by model entries should not expose sessionId"
);
}
}
fn add_alias_variant_message(tmp: &Path) {
let session3 = tmp.join(".local/share/opencode/storage/message/session3");
fs::create_dir_all(&session3).unwrap();
let msg = r#"{
"id": "msg_d",
"sessionID": "session3",
"role": "assistant",
"modelID": "claude-sonnet-4-cc",
"providerID": "anthropic",
"cost": 0.04,
"tokens": {
"input": 400,
"output": 100,
"reasoning": 0,
"cache": { "read": 0, "write": 0 }
},
"time": { "created": 1718460000000.0, "completed": 1718460002000.0 }
}"#;
fs::write(session3.join("msg_d.json"), msg).unwrap();
}
fn write_model_aliases(tmp: &Path, aliases_json: &str) {
let config_dir = tmp.join(".config/tokmesh");
fs::create_dir_all(&config_dir).unwrap();
fs::write(
config_dir.join("settings.json"),
format!(r#"{{"modelAliases": {aliases_json}}}"#),
)
.unwrap();
}
fn models_by_name(tmp: &Path) -> serde_json::Value {
let output = cmd_with_home(tmp)
.args(["models", "--json", "--client", "opencode", "--no-spinner"])
.args(["--group-by", "model"])
.output()
.unwrap();
assert!(output.status.success(), "command failed: {output:?}");
serde_json::from_slice(&output.stdout).unwrap()
}
#[test]
fn test_models_alias_folds_variants_into_one_row() {
let tmp = create_temp_fixture_dir();
add_alias_variant_message(tmp.path());
write_model_aliases(tmp.path(), r#"{"claude-sonnet-4-cc": "claude-sonnet-4"}"#);
let json = models_by_name(tmp.path());
let entries = json["entries"].as_array().unwrap();
let models: Vec<&str> = entries
.iter()
.map(|e| e["model"].as_str().unwrap())
.collect();
assert!(
models.contains(&"claude-sonnet-4"),
"expected folded model, got {models:?}"
);
assert!(
!models.contains(&"claude-sonnet-4-cc"),
"variant should have folded away, got {models:?}"
);
let folded = entries
.iter()
.find(|e| e["model"] == "claude-sonnet-4")
.unwrap();
assert_eq!(
folded["input"].as_i64().unwrap(),
2200,
"folded row must include the variant's tokens, got {folded:?}"
);
assert!(
folded.get("mergedClients").is_some(),
"folded entry should retain mergedClients"
);
let sum_input: i64 = entries.iter().map(|e| e["input"].as_i64().unwrap()).sum();
assert_eq!(sum_input, json["totalInput"].as_i64().unwrap());
}
#[test]
fn test_models_alias_folds_in_monthly_report() {
let tmp = create_temp_fixture_dir();
add_alias_variant_message(tmp.path());
write_model_aliases(tmp.path(), r#"{"claude-sonnet-4-cc": "claude-sonnet-4"}"#);
let output = cmd_with_home(tmp.path())
.args(["monthly", "--json", "--client", "opencode", "--no-spinner"])
.output()
.unwrap();
assert!(output.status.success(), "command failed: {output:?}");
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let models: Vec<&str> = json["entries"]
.as_array()
.unwrap()
.iter()
.flat_map(|entry| entry["models"].as_array().unwrap())
.map(|m| m.as_str().unwrap())
.collect();
assert!(
models.contains(&"claude-sonnet-4"),
"monthly models should include the canonical name, got {models:?}"
);
assert!(
!models.contains(&"claude-sonnet-4-cc"),
"monthly report must fold the -cc variant too, got {models:?}"
);
}
#[test]
fn test_models_alias_absent_is_noop() {
let tmp = create_temp_fixture_dir();
add_alias_variant_message(tmp.path());
let json = models_by_name(tmp.path());
let models: Vec<&str> = json["entries"]
.as_array()
.unwrap()
.iter()
.map(|e| e["model"].as_str().unwrap())
.collect();
assert!(
models.contains(&"claude-sonnet-4"),
"expected base model, got {models:?}"
);
assert!(
models.contains(&"claude-sonnet-4-cc"),
"variant should remain separate without aliases, got {models:?}"
);
}
#[test]
fn test_models_alias_totals_unchanged() {
let without = create_temp_fixture_dir();
add_alias_variant_message(without.path());
let base = models_by_name(without.path());
let with = create_temp_fixture_dir();
add_alias_variant_message(with.path());
write_model_aliases(with.path(), r#"{"claude-sonnet-4-cc": "claude-sonnet-4"}"#);
let aliased = models_by_name(with.path());
assert_eq!(
base["totalInput"].as_i64(),
aliased["totalInput"].as_i64(),
"totalInput must be unchanged by aliasing"
);
assert_eq!(
base["totalOutput"].as_i64(),
aliased["totalOutput"].as_i64(),
"totalOutput must be unchanged by aliasing"
);
let base_cost = base["totalCost"].as_f64().unwrap();
let aliased_cost = aliased["totalCost"].as_f64().unwrap();
assert!(
(base_cost - aliased_cost).abs() < 1e-9,
"totalCost must be unchanged by aliasing: {base_cost} vs {aliased_cost}"
);
}
#[test]
fn test_alias_folds_local_report_but_not_submitted_payload() {
let tmp = create_temp_fixture_dir();
add_alias_variant_message(tmp.path());
write_model_aliases(tmp.path(), r#"{"claude-sonnet-4-cc": "claude-sonnet-4"}"#);
let models = models_by_name(tmp.path());
let displayed: Vec<&str> = models["entries"]
.as_array()
.unwrap()
.iter()
.map(|e| e["model"].as_str().unwrap())
.collect();
assert!(
displayed.contains(&"claude-sonnet-4"),
"local report must show the canonical name, got {displayed:?}"
);
assert!(
!displayed.contains(&"claude-sonnet-4-cc"),
"local report must fold the -cc variant away, got {displayed:?}"
);
let output = cmd_with_home(tmp.path())
.args(["graph", "--client", "opencode", "--no-spinner"])
.output()
.unwrap();
assert!(output.status.success(), "graph command failed: {output:?}");
let payload: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let submitted_models: Vec<&str> = payload["summary"]["models"]
.as_array()
.unwrap()
.iter()
.map(|m| m.as_str().unwrap())
.collect();
assert!(
submitted_models.contains(&"claude-sonnet-4-cc"),
"submitted payload must keep the RAW model id (alias must not leak), got {submitted_models:?}"
);
let per_contribution_models: Vec<&str> = payload["contributions"]
.as_array()
.unwrap()
.iter()
.flat_map(|c| c["clients"].as_array().unwrap())
.map(|s| s["modelId"].as_str().unwrap())
.collect();
assert!(
per_contribution_models.contains(&"claude-sonnet-4-cc"),
"submitted per-day contributions must carry the RAW model id, got {per_contribution_models:?}"
);
}
#[test]
fn test_models_group_by_session_emits_session_id_per_entry() {
let tmp = create_temp_fixture_dir();
let output = cmd_with_home(tmp.path())
.args(["models", "--json", "--client", "opencode", "--no-spinner"])
.args(["--group-by", "session,model"])
.output()
.unwrap();
assert!(output.status.success(), "command failed: {:?}", output);
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(json["groupBy"].as_str().unwrap(), "session,model");
let entries = json["entries"].as_array().unwrap();
assert!(!entries.is_empty(), "expected at least one entry");
let mut session_ids: Vec<&str> = entries
.iter()
.map(|e| {
e.get("sessionId")
.and_then(|v| v.as_str())
.expect("session,model entries must include sessionId")
})
.collect();
session_ids.sort();
session_ids.dedup();
assert!(
session_ids.contains(&"session1") && session_ids.contains(&"session2"),
"expected both fixture sessions to appear in output, got {:?}",
session_ids
);
for entry in entries {
assert!(
entry.get("workspaceKey").is_none(),
"session grouping should not expose workspaceKey"
);
assert!(entry.get("model").is_some());
assert!(entry.get("provider").is_some());
assert!(entry.get("cost").is_some());
}
}
#[test]
fn test_models_group_by_client_session_includes_client_and_session() {
let tmp = create_temp_fixture_dir();
let output = cmd_with_home(tmp.path())
.args(["models", "--json", "--client", "opencode", "--no-spinner"])
.args(["--group-by", "client,session,model"])
.output()
.unwrap();
assert!(output.status.success(), "command failed: {:?}", output);
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(json["groupBy"].as_str().unwrap(), "client,session,model");
let entries = json["entries"].as_array().unwrap();
assert!(!entries.is_empty());
for entry in entries {
assert!(entry.get("sessionId").and_then(|v| v.as_str()).is_some());
assert!(entry.get("client").and_then(|v| v.as_str()).is_some());
assert!(entry.get("model").is_some());
}
}
#[test]
fn test_models_group_by_workspace_model_uses_unknown_bucket_for_unsupported_clients() {
let tmp = create_temp_fixture_dir();
let output = cmd_with_home(tmp.path())
.args(["models", "--json", "--client", "opencode", "--no-spinner"])
.args(["--group-by", "workspace,model"])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(json["groupBy"].as_str().unwrap(), "workspace,model");
let entries = json["entries"].as_array().unwrap();
assert!(!entries.is_empty());
for entry in entries {
assert!(
entry.get("workspaceKey").is_some(),
"workspace grouping entries should always expose workspaceKey"
);
assert!(entry["workspaceKey"].is_null());
assert!(
entry.get("workspaceLabel").is_some(),
"workspace grouping entries should always expose workspaceLabel"
);
assert_eq!(
entry["workspaceLabel"].as_str().unwrap(),
"Unknown workspace"
);
}
}
#[test]
fn test_models_group_by_workspace_model_surfaces_workspace_fields_for_qwen() {
let tmp = create_qwen_workspace_fixture_dir();
let output = cmd_with_home(tmp.path())
.args(["models", "--json", "--client", "qwen", "--no-spinner"])
.args(["--group-by", "workspace-model"])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(json["groupBy"].as_str().unwrap(), "workspace,model");
let entries = json["entries"].as_array().unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(
entries[0]["workspaceKey"].as_str().unwrap(),
"demo-workspace"
);
assert_eq!(
entries[0]["workspaceLabel"].as_str().unwrap(),
"demo-workspace"
);
assert_eq!(entries[0]["model"].as_str().unwrap(), "qwen3.5-plus");
}
#[test]
fn test_models_group_by_workspace_model_surfaces_workspace_fields_for_codex() {
let tmp = create_codex_workspace_fixture_dir();
let output = cmd_with_home(tmp.path())
.args(["models", "--json", "--client", "codex", "--no-spinner"])
.args(["--group-by", "workspace,model"])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(json["groupBy"].as_str().unwrap(), "workspace,model");
let entries = json["entries"].as_array().unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(
entries[0]["workspaceKey"].as_str().unwrap(),
"/Users/alice/codex-workspace"
);
assert_eq!(
entries[0]["workspaceLabel"].as_str().unwrap(),
"codex-workspace"
);
assert_eq!(entries[0]["model"].as_str().unwrap(), "gpt-5.4");
}
#[test]
fn test_models_group_by_workspace_model_surfaces_workspace_fields_for_opencode() {
let tmp = create_opencode_workspace_fixture_dir();
let output = cmd_with_home(tmp.path())
.args(["models", "--json", "--client", "opencode", "--no-spinner"])
.args(["--group-by", "workspace,model"])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(json["groupBy"].as_str().unwrap(), "workspace,model");
let entries = json["entries"].as_array().unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(
entries[0]["workspaceKey"].as_str().unwrap(),
"/Users/alice/opencode-workspace"
);
assert_eq!(
entries[0]["workspaceLabel"].as_str().unwrap(),
"opencode-workspace"
);
assert_eq!(entries[0]["model"].as_str().unwrap(), "claude-sonnet-4");
}
#[test]
fn test_pricing_command_success() {
let mut cmd = cargo_bin_cmd!("tokmesh");
cmd.args(["pricing", "claude-sonnet-4-20250514", "--no-spinner"])
.assert()
.success()
.stdout(predicate::str::contains("Pricing for"))
.stdout(predicate::str::contains("Input"))
.stdout(predicate::str::contains("Output"));
}
#[test]
fn test_pricing_command_json() {
let output = cargo_bin_cmd!("tokmesh")
.args([
"pricing",
"claude-sonnet-4-20250514",
"--json",
"--no-spinner",
])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert!(json.get("modelId").is_some(), "Missing modelId");
assert!(json.get("matchedKey").is_some(), "Missing matchedKey");
assert!(json.get("source").is_some(), "Missing source");
assert!(json.get("pricing").is_some(), "Missing pricing");
let pricing = &json["pricing"];
assert!(pricing.get("inputCostPerToken").is_some());
assert!(pricing.get("outputCostPerToken").is_some());
}
#[test]
fn test_pricing_command_with_provider() {
let mut cmd = cargo_bin_cmd!("tokmesh");
cmd.args([
"pricing",
"claude-sonnet-4-20250514",
"--provider",
"litellm",
"--no-spinner",
])
.assert()
.success();
}
#[test]
fn test_pricing_command_invalid_provider() {
let mut cmd = cargo_bin_cmd!("tokmesh");
cmd.args([
"pricing",
"claude-sonnet-4-20250514",
"--provider",
"invalid-provider",
"--no-spinner",
])
.assert()
.failure();
}
#[test]
fn test_pricing_command_does_not_fuzzy_match_provider_scoped_fireworks_model() {
let tmp = TempDir::new().expect("failed to create temp dir");
write_fireworks_pricing_cache(tmp.path());
let output = cmd_with_home(tmp.path())
.args([
"pricing",
"accounts/fireworks/models/deepseek-v4-pro",
"--no-spinner",
])
.output()
.unwrap();
assert!(!output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("Model not found: accounts/fireworks/models/deepseek-v4-pro"),
"expected a not-found message, got: {stdout}"
);
assert!(
!stdout.contains("deepseek-r1-0528-distill-qwen3-8b"),
"provider-scoped pricing lookup must not report the wrong Fireworks match: {stdout}"
);
}
#[test]
fn test_clients_command() {
let tmp = create_empty_fixture_dir();
cmd_with_home(tmp.path())
.arg("clients")
.assert()
.success()
.stdout(predicate::str::contains("OpenCode").or(predicate::str::contains("opencode")))
.stdout(predicate::str::contains("Claude").or(predicate::str::contains("claude")));
}
#[test]
fn test_clients_json() {
let tmp = create_empty_fixture_dir();
let output = cmd_with_home(tmp.path())
.args(["clients", "--json"])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert!(json.is_object(), "Clients JSON should be an object");
assert!(json.get("clients").is_some(), "Should have 'clients' field");
assert!(
json.get("headlessRoots").is_some(),
"Should have 'headlessRoots' field"
);
assert!(json.get("note").is_some(), "Should have 'note' field");
let arr = json["clients"].as_array().unwrap();
assert!(!arr.is_empty(), "Should list at least one client");
let first = &arr[0];
assert!(
first.get("client").is_some(),
"Client entry should have 'client' field"
);
assert!(
first.get("label").is_some(),
"Client entry should have 'label' field"
);
assert!(
first.get("sessionsPath").is_some(),
"Client entry should have 'sessionsPath' field"
);
assert!(
first.get("messageCount").is_some(),
"Client entry should have 'messageCount' field"
);
}
#[test]
fn test_clients_json_includes_claude_transcripts_path() {
let tmp = create_empty_fixture_dir();
fs::create_dir_all(tmp.path().join(".claude/transcripts")).unwrap();
let output = cmd_with_home(tmp.path())
.args(["clients", "--json"])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let claude = json["clients"]
.as_array()
.unwrap()
.iter()
.find(|row| row["client"] == "claude")
.unwrap();
assert_eq!(
claude["additionalPaths"][0]["path"],
serde_json::json!(tmp.path().join(".claude/transcripts"))
);
assert_eq!(claude["additionalPaths"][0]["exists"], true);
}
#[test]
fn test_clients_command_includes_claude_transcripts_text() {
let tmp = create_empty_fixture_dir();
fs::create_dir_all(tmp.path().join(".claude/transcripts")).unwrap();
cmd_with_home(tmp.path())
.arg("clients")
.assert()
.success()
.stdout(predicate::str::contains(
"additional: ~/.claude/transcripts ✓",
));
}
#[test]
fn test_clients_json_includes_claude_desktop_diagnostic() {
let tmp = create_empty_fixture_dir();
fs::create_dir_all(tmp.path().join("Library/Application Support/Claude")).unwrap();
let output = cmd_with_home(tmp.path())
.args(["clients", "--json"])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let claude = json["clients"]
.as_array()
.unwrap()
.iter()
.find(|row| row["client"] == "claude")
.unwrap();
let diagnostics = claude["diagnostics"].as_array().unwrap();
assert!(diagnostics.iter().any(|item| {
item["code"] == "claude_desktop_not_scanned"
&& item["severity"] == "warning"
&& item["message"]
.as_str()
.unwrap()
.contains("Claude Desktop app data was detected")
}));
}
#[test]
fn test_clients_command_includes_claude_desktop_diagnostic_text() {
let tmp = create_empty_fixture_dir();
fs::create_dir_all(tmp.path().join("Library/Application Support/Claude")).unwrap();
cmd_with_home(tmp.path())
.arg("clients")
.assert()
.success()
.stdout(predicate::str::contains(
"Claude Desktop app data was detected",
))
.stdout(predicate::str::contains(
"Claude Code JSONL transcripts only",
));
}
#[test]
fn test_models_json_includes_claude_desktop_diagnostic_for_empty_explicit_claude_report() {
let tmp = create_empty_fixture_dir();
fs::create_dir_all(tmp.path().join("Library/Application Support/Claude")).unwrap();
let output = cmd_with_home(tmp.path())
.args(["models", "--client", "claude", "--json", "--no-spinner"])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let diagnostics = json["diagnostics"].as_array().unwrap();
assert!(diagnostics.iter().any(|item| {
item["code"] == "claude_desktop_not_scanned"
&& item["message"]
.as_str()
.unwrap()
.contains("Tokmesh counts Claude Code JSONL transcripts")
}));
}
#[test]
fn test_clients_json_includes_settings_extra_paths() {
let tmp = create_empty_fixture_dir();
write_settings_json(
tmp.path(),
r#"{
"scanner": {
"extraScanPaths": {
"codex": ["/tmp/project-a/.codex/sessions"]
}
}
}"#,
);
let output = cmd_with_home(tmp.path())
.args(["clients", "--json"])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let codex = json["clients"]
.as_array()
.unwrap()
.iter()
.find(|row| row["client"] == "codex")
.unwrap();
assert_eq!(
codex["extraPaths"][0]["path"],
serde_json::json!("/tmp/project-a/.codex/sessions")
);
assert_eq!(
codex["extraPaths"][0]["source"],
serde_json::json!("settings")
);
}
#[test]
fn test_clients_json_includes_hermes_settings_extra_profile_path() {
let tmp = create_empty_fixture_dir();
let hermes_profile = tmp.path().join(".hermes/profiles/director_planning");
fs::create_dir_all(&hermes_profile).unwrap();
let hermes_profile_json = serde_json::to_string(&hermes_profile).unwrap();
write_settings_json(
tmp.path(),
&format!(
r#"{{
"scanner": {{
"extraScanPaths": {{
"hermes": [{hermes_profile_json}]
}}
}}
}}"#
),
);
let output = cmd_with_home(tmp.path())
.args(["clients", "--json"])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let hermes = json["clients"]
.as_array()
.unwrap()
.iter()
.find(|row| row["client"] == "hermes")
.unwrap();
assert_eq!(
hermes["extraPaths"][0]["path"],
serde_json::json!(hermes_profile)
);
assert_eq!(
hermes["extraPaths"][0]["source"],
serde_json::json!("settings")
);
assert_eq!(hermes["extraPaths"][0]["exists"], true);
}
#[test]
fn test_clients_command_includes_settings_extra_paths_text() {
let tmp = create_empty_fixture_dir();
write_settings_json(
tmp.path(),
r#"{
"scanner": {
"extraScanPaths": {
"codex": ["/tmp/project-a/.codex/sessions"]
}
}
}"#,
);
cmd_with_home(tmp.path())
.arg("clients")
.assert()
.success()
.stdout(predicate::str::contains(
"extra (settings): /tmp/project-a/.codex/sessions ✗",
));
}
#[test]
fn test_models_light_output() {
let tmp = create_temp_fixture_dir();
cmd_with_home(tmp.path())
.args(["models", "--light", "--client", "opencode", "--no-spinner"])
.assert()
.success()
.stdout(predicate::str::contains("Token Usage Report by Model"))
.stdout(predicate::str::contains("ms/1K"));
}
#[test]
fn test_monthly_light_output() {
let tmp = create_temp_fixture_dir();
cmd_with_home(tmp.path())
.args(["monthly", "--light", "--client", "opencode", "--no-spinner"])
.assert()
.success()
.stdout(predicate::str::contains("Monthly Token Usage Report"));
}
#[test]
fn test_models_light_with_client_filter() {
let tmp = create_temp_fixture_dir();
cmd_with_home(tmp.path())
.args(["models", "--light", "--client", "opencode", "--no-spinner"])
.args(["--year", "2024"])
.assert()
.success()
.stdout(predicate::str::contains("2024"));
}
#[test]
fn test_models_benchmark_flag() {
let tmp = create_temp_fixture_dir();
cmd_with_home(tmp.path())
.args([
"models",
"--light",
"--client",
"opencode",
"--no-spinner",
"--benchmark",
])
.assert()
.success()
.stdout(predicate::str::contains("Processing time"));
}
#[test]
fn test_monthly_benchmark_flag() {
let tmp = create_temp_fixture_dir();
cmd_with_home(tmp.path())
.args([
"monthly",
"--light",
"--client",
"opencode",
"--no-spinner",
"--benchmark",
])
.assert()
.success()
.stdout(predicate::str::contains("Processing time"));
}
#[test]
fn test_models_empty_fixture() {
let tmp = create_empty_fixture_dir();
let output = cmd_with_home(tmp.path())
.args(["models", "--json", "--client", "opencode", "--no-spinner"])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let entries = json["entries"].as_array().unwrap();
assert!(
entries.is_empty(),
"Empty fixture should produce no entries"
);
assert_eq!(json["totalInput"].as_i64().unwrap(), 0);
assert_eq!(json["totalOutput"].as_i64().unwrap(), 0);
}
#[test]
fn test_graph_empty_contributions() {
let tmp = create_empty_fixture_dir();
let output = cmd_with_home(tmp.path())
.args(["graph", "--client", "opencode", "--no-spinner"])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let contributions = json["contributions"].as_array().unwrap();
assert!(
contributions.is_empty(),
"Empty fixture should produce no contributions"
);
}
#[test]
fn test_models_no_spinner_flag() {
let tmp = create_temp_fixture_dir();
cmd_with_home(tmp.path())
.args(["models", "--light", "--client", "opencode", "--no-spinner"])
.assert()
.success();
}
#[test]
fn test_graph_no_spinner_flag() {
let tmp = create_temp_fixture_dir();
cmd_with_home(tmp.path())
.args(["graph", "--client", "opencode", "--no-spinner"])
.assert()
.success();
}
#[test]
fn test_graph_with_client_filter() {
let tmp = create_temp_fixture_dir();
let output = cmd_with_home(tmp.path())
.args(["graph", "--client", "opencode", "--no-spinner"])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let contributions = json["contributions"].as_array().unwrap();
for c in contributions {
let clients = c["clients"].as_array().unwrap();
for cl in clients {
assert_eq!(
cl["client"].as_str().unwrap(),
"opencode",
"All contributions should be from opencode"
);
}
}
}
#[test]
fn test_graph_output_to_file() {
let tmp = create_temp_fixture_dir();
let output_file = tmp.path().join("graph-output.json");
cmd_with_home(tmp.path())
.args(["graph", "--client", "opencode", "--no-spinner"])
.args(["--output", output_file.to_str().unwrap()])
.assert()
.success();
assert!(output_file.exists(), "Output file should be created");
let content = fs::read_to_string(&output_file).unwrap();
let json: serde_json::Value = serde_json::from_str(&content).unwrap();
assert!(json.get("meta").is_some());
assert!(json.get("contributions").is_some());
}
#[test]
fn test_root_json_output() {
let tmp = create_temp_fixture_dir();
let output = cmd_with_home(tmp.path())
.args(["--json", "--client", "opencode", "--no-spinner"])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert!(json.get("entries").is_some());
assert!(json.get("totalCost").is_some());
}
#[test]
fn test_root_light_output() {
let tmp = create_temp_fixture_dir();
cmd_with_home(tmp.path())
.args(["--light", "--client", "opencode", "--no-spinner"])
.assert()
.success()
.stdout(predicate::str::contains("Token Usage Report by Model"));
}
#[test]
fn light_with_write_cache_writes_to_canonical_path() {
let tmp = create_temp_fixture_dir();
let config_dir = tmp.path().join("custom-config-root");
prime_override_pricing_cache(&config_dir);
cmd_with_home(tmp.path())
.env("TOKMESH_CONFIG_DIR", &config_dir)
.args([
"--light",
"--client",
"opencode",
"--write-cache",
"--no-spinner",
])
.assert()
.success()
.stdout(predicate::str::contains("Token Usage Report by Model"));
assert!(
config_dir.join("cache/tui-data-cache.json").exists(),
"--write-cache should populate the canonical cache path"
);
}
#[test]
fn test_root_with_date_filter() {
let tmp = create_temp_fixture_dir();
cmd_with_home(tmp.path())
.args(["--json", "--client", "opencode", "--no-spinner"])
.args(["--year", "2025"])
.assert()
.success()
.stdout(predicate::str::contains("gpt-4o"));
}
#[test]
fn test_root_with_group_by() {
let tmp = create_temp_fixture_dir();
let output = cmd_with_home(tmp.path())
.args(["--json", "--client", "opencode", "--no-spinner"])
.args(["--group-by", "model"])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(json["groupBy"].as_str().unwrap(), "model");
}
#[test]
fn test_submit_offline_without_pricing_cache_fails() {
let tmp = create_temp_fixture_dir_without_pricing_cache();
write_fake_credentials(tmp.path());
let output = offline_cmd_with_home(tmp.path())
.env("TOKMESH_CONFIG_DIR", tmp.path().join(".config/tokmesh"))
.args(["tokscale", "submit", "--client", "opencode", "--dry-run"])
.output()
.unwrap();
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!output.status.success(),
"submit should fail when pricing is unavailable; stdout: {}",
String::from_utf8_lossy(&output.stdout)
);
assert!(
!stderr.contains("Not logged in"),
"submit failed due to auth, not pricing: {stderr}"
);
assert!(
stderr.contains("error") || stderr.contains("Error"),
"stderr should contain a pricing/network error: {stderr}"
);
}
fn write_gjc_session_fixture(base: &Path) {
let session_dir = base.join(".gjc/agent/sessions/test-project");
fs::create_dir_all(&session_dir).unwrap();
let jsonl = concat!(
r#"{"type":"session","id":"gjc_e2e_session","timestamp":"2025-06-15T12:00:00.000Z","cwd":"/work/test-project"}"#,
"\n",
r#"{"type":"message","id":"gjc_e2e_msg_1","parentId":null,"timestamp":"2025-06-15T12:00:01.000Z","message":{"role":"assistant","model":"claude-sonnet-4","provider":"anthropic","api":"anthropic","timestamp":1750082401000,"usage":{"input":1000,"output":500,"cacheRead":0,"cacheWrite":0,"totalTokens":1500,"cost":{"input":0.3,"output":0.2,"cacheRead":0.0,"cacheWrite":0.0,"total":0.5}}}}"#,
"\n"
);
fs::write(session_dir.join("sess.jsonl"), jsonl).unwrap();
}
fn gjc_cmd_with_home(tmp: &Path) -> Command {
let mut cmd = cmd_with_home(tmp);
cmd.env_remove("GJC_CODING_AGENT_DIR")
.env_remove("GJC_CONFIG_DIR")
.env_remove("PI_CONFIG_DIR");
cmd
}
#[test]
fn test_models_with_client_filter_gjc() {
let tmp = TempDir::new().expect("failed to create temp dir");
prime_pricing_cache(tmp.path());
write_gjc_session_fixture(tmp.path());
let output = gjc_cmd_with_home(tmp.path())
.args(["models", "--json", "--client", "gjc", "--no-spinner"])
.output()
.unwrap();
assert!(
output.status.success(),
"command failed; stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let entries = json["entries"]
.as_array()
.expect("entries must be an array");
assert!(
!entries.is_empty(),
"expected gjc entries but got none; full JSON: {json}"
);
for entry in entries {
assert_eq!(
entry["client"].as_str().unwrap_or(""),
"gjc",
"unexpected client in entry: {entry}"
);
}
let has_sonnet = entries.iter().any(|e| {
e["model"]
.as_str()
.unwrap_or("")
.contains("claude-sonnet-4")
});
assert!(
has_sonnet,
"expected claude-sonnet-4 in gjc entries; got: {entries:?}"
);
}
#[test]
fn test_client_filter_gjc_empty_is_clean() {
let tmp = TempDir::new().expect("failed to create temp dir");
prime_pricing_cache(tmp.path());
let output = gjc_cmd_with_home(tmp.path())
.args(["models", "--json", "--client", "gjc", "--no-spinner"])
.output()
.unwrap();
assert!(
output.status.success(),
"command failed with no gjc data; stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let entries = json["entries"]
.as_array()
.expect("entries must be an array");
assert!(
entries.is_empty(),
"expected zero entries for empty gjc fixture, got: {entries:?}"
);
}
#[test]
fn test_client_filter_gjc_isolation() {
let tmp = TempDir::new().expect("failed to create temp dir");
prime_pricing_cache(tmp.path());
write_gjc_session_fixture(tmp.path());
let output = gjc_cmd_with_home(tmp.path())
.args(["models", "--json", "--client", "claude", "--no-spinner"])
.output()
.unwrap();
assert!(
output.status.success(),
"command failed; stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let entries = json["entries"]
.as_array()
.expect("entries must be an array");
for entry in entries {
assert_ne!(
entry["client"].as_str().unwrap_or(""),
"gjc",
"gjc entry leaked into --client claude output: {entry}"
);
}
}
#[test]
fn report_no_summarize_json_empty_home_emits_valid_json_without_panic() {
let tmp = create_empty_fixture_dir();
let output = cmd_with_home(tmp.path())
.arg("report")
.arg("--no-summarize")
.arg("--json")
.output()
.unwrap();
assert!(
output.status.success(),
"report --no-summarize --json failed against empty home; stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value =
serde_json::from_slice(&output.stdout).expect("report --json must emit valid JSON");
let entries = json
.as_array()
.expect("report --json output must be a JSON array of entries");
assert!(
entries.is_empty(),
"expected zero entries for empty home, got: {entries:?}"
);
}