mirror of
https://github.com/openai/codex.git
synced 2026-05-02 12:21:26 +03:00
Problem: The TUI still depended on `codex-core` directly in a number of places, and we had no enforcement from keeping this problem from getting worse. Solution: Route TUI core access through `codex-app-server-client::legacy_core`, add CI enforcement for that boundary, and re-export this legacy bridge inside the TUI as `crate::legacy_core` so the remaining call sites stay readable. There is no functional change in this PR — just changes to import targets. Over time, we can whittle away at the remaining symbols in this legacy namespace with the eventual goal of removing them all. In the meantime, this linter rule will prevent us from inadvertently importing new symbols from core.
1151 lines
37 KiB
Rust
1151 lines
37 KiB
Rust
use super::new_status_output;
|
|
use super::new_status_output_with_rate_limits;
|
|
use super::rate_limit_snapshot_display;
|
|
use crate::history_cell::HistoryCell;
|
|
use crate::legacy_core::config::Config;
|
|
use crate::legacy_core::config::ConfigBuilder;
|
|
use crate::status::StatusAccountDisplay;
|
|
use crate::test_support::PathBufExt;
|
|
use chrono::Duration as ChronoDuration;
|
|
use chrono::TimeZone;
|
|
use chrono::Utc;
|
|
use codex_protocol::ThreadId;
|
|
use codex_protocol::config_types::ReasoningSummary;
|
|
use codex_protocol::openai_models::ReasoningEffort;
|
|
use codex_protocol::protocol::AskForApproval;
|
|
use codex_protocol::protocol::CreditsSnapshot;
|
|
use codex_protocol::protocol::RateLimitSnapshot;
|
|
use codex_protocol::protocol::RateLimitWindow;
|
|
use codex_protocol::protocol::SandboxPolicy;
|
|
use codex_protocol::protocol::TokenUsage;
|
|
use codex_protocol::protocol::TokenUsageInfo;
|
|
use insta::assert_snapshot;
|
|
use pretty_assertions::assert_eq;
|
|
use ratatui::prelude::*;
|
|
use std::path::PathBuf;
|
|
use tempfile::TempDir;
|
|
|
|
async fn test_config(temp_home: &TempDir) -> Config {
|
|
ConfigBuilder::default()
|
|
.codex_home(temp_home.path().to_path_buf())
|
|
.build()
|
|
.await
|
|
.expect("load config")
|
|
}
|
|
|
|
fn test_status_account_display() -> Option<StatusAccountDisplay> {
|
|
None
|
|
}
|
|
|
|
fn token_info_for(model_slug: &str, config: &Config, usage: &TokenUsage) -> TokenUsageInfo {
|
|
let context_window =
|
|
crate::legacy_core::test_support::construct_model_info_offline(model_slug, config)
|
|
.context_window;
|
|
TokenUsageInfo {
|
|
total_token_usage: usage.clone(),
|
|
last_token_usage: usage.clone(),
|
|
model_context_window: context_window,
|
|
}
|
|
}
|
|
|
|
fn render_lines(lines: &[Line<'static>]) -> Vec<String> {
|
|
lines
|
|
.iter()
|
|
.map(|line| {
|
|
line.spans
|
|
.iter()
|
|
.map(|span| span.content.as_ref())
|
|
.collect::<String>()
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn sanitize_directory(lines: Vec<String>) -> Vec<String> {
|
|
lines
|
|
.into_iter()
|
|
.map(|line| {
|
|
if let (Some(dir_pos), Some(pipe_idx)) = (line.find("Directory: "), line.rfind('│')) {
|
|
let prefix = &line[..dir_pos + "Directory: ".len()];
|
|
let suffix = &line[pipe_idx..];
|
|
let content_width = pipe_idx.saturating_sub(dir_pos + "Directory: ".len());
|
|
let replacement = "[[workspace]]";
|
|
let mut rebuilt = prefix.to_string();
|
|
rebuilt.push_str(replacement);
|
|
if content_width > replacement.len() {
|
|
rebuilt.push_str(&" ".repeat(content_width - replacement.len()));
|
|
}
|
|
rebuilt.push_str(suffix);
|
|
rebuilt
|
|
} else {
|
|
line
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn reset_at_from(captured_at: &chrono::DateTime<chrono::Local>, seconds: i64) -> i64 {
|
|
(*captured_at + ChronoDuration::seconds(seconds))
|
|
.with_timezone(&Utc)
|
|
.timestamp()
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn status_snapshot_includes_reasoning_details() {
|
|
let temp_home = TempDir::new().expect("temp home");
|
|
let mut config = test_config(&temp_home).await;
|
|
config.model = Some("gpt-5.1-codex-max".to_string());
|
|
config.model_provider_id = "openai".to_string();
|
|
config.model_reasoning_summary = Some(ReasoningSummary::Detailed);
|
|
config
|
|
.permissions
|
|
.sandbox_policy
|
|
.set(SandboxPolicy::WorkspaceWrite {
|
|
writable_roots: Vec::new(),
|
|
read_only_access: Default::default(),
|
|
network_access: false,
|
|
exclude_tmpdir_env_var: false,
|
|
exclude_slash_tmp: false,
|
|
})
|
|
.expect("set sandbox policy");
|
|
|
|
config.cwd = PathBuf::from("/workspace/tests").abs();
|
|
|
|
let account_display = test_status_account_display();
|
|
let usage = TokenUsage {
|
|
input_tokens: 1_200,
|
|
cached_input_tokens: 200,
|
|
output_tokens: 900,
|
|
reasoning_output_tokens: 150,
|
|
total_tokens: 2_250,
|
|
};
|
|
|
|
let captured_at = chrono::Local
|
|
.with_ymd_and_hms(2024, 1, 2, 3, 4, 5)
|
|
.single()
|
|
.expect("timestamp");
|
|
let snapshot = RateLimitSnapshot {
|
|
limit_id: None,
|
|
limit_name: None,
|
|
primary: Some(RateLimitWindow {
|
|
used_percent: 72.5,
|
|
window_minutes: Some(300),
|
|
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 600)),
|
|
}),
|
|
secondary: Some(RateLimitWindow {
|
|
used_percent: 45.0,
|
|
window_minutes: Some(10080),
|
|
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 1_200)),
|
|
}),
|
|
credits: None,
|
|
plan_type: None,
|
|
};
|
|
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
|
|
|
let model_slug = crate::legacy_core::test_support::get_model_offline(config.model.as_deref());
|
|
let token_info = token_info_for(&model_slug, &config, &usage);
|
|
|
|
let reasoning_effort_override = Some(Some(ReasoningEffort::High));
|
|
let composite = new_status_output(
|
|
&config,
|
|
account_display.as_ref(),
|
|
Some(&token_info),
|
|
&usage,
|
|
&None,
|
|
/*thread_name*/ None,
|
|
/*forked_from*/ None,
|
|
Some(&rate_display),
|
|
None,
|
|
captured_at,
|
|
&model_slug,
|
|
/*collaboration_mode*/ None,
|
|
reasoning_effort_override,
|
|
);
|
|
let mut rendered_lines = render_lines(&composite.display_lines(/*width*/ 80));
|
|
if cfg!(windows) {
|
|
for line in &mut rendered_lines {
|
|
*line = line.replace('\\', "/");
|
|
}
|
|
}
|
|
let sanitized = sanitize_directory(rendered_lines).join("\n");
|
|
assert_snapshot!(sanitized);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn status_permissions_non_default_workspace_write_is_custom() {
|
|
let temp_home = TempDir::new().expect("temp home");
|
|
let mut config = test_config(&temp_home).await;
|
|
config.model = Some("gpt-5.1-codex-max".to_string());
|
|
config.model_provider_id = "openai".to_string();
|
|
config
|
|
.permissions
|
|
.approval_policy
|
|
.set(AskForApproval::OnRequest)
|
|
.expect("set approval policy");
|
|
config
|
|
.permissions
|
|
.sandbox_policy
|
|
.set(SandboxPolicy::WorkspaceWrite {
|
|
writable_roots: Vec::new(),
|
|
read_only_access: Default::default(),
|
|
network_access: true,
|
|
exclude_tmpdir_env_var: false,
|
|
exclude_slash_tmp: false,
|
|
})
|
|
.expect("set sandbox policy");
|
|
config.cwd = PathBuf::from("/workspace/tests").abs();
|
|
|
|
let account_display = test_status_account_display();
|
|
let usage = TokenUsage::default();
|
|
let captured_at = chrono::Local
|
|
.with_ymd_and_hms(2024, 1, 2, 3, 4, 5)
|
|
.single()
|
|
.expect("timestamp");
|
|
let model_slug = crate::legacy_core::test_support::get_model_offline(config.model.as_deref());
|
|
|
|
let composite = new_status_output(
|
|
&config,
|
|
account_display.as_ref(),
|
|
/*token_info*/ None,
|
|
&usage,
|
|
&None,
|
|
/*thread_name*/ None,
|
|
/*forked_from*/ None,
|
|
/*rate_limits*/ None,
|
|
None,
|
|
captured_at,
|
|
&model_slug,
|
|
/*collaboration_mode*/ None,
|
|
/*reasoning_effort_override*/ None,
|
|
);
|
|
let rendered_lines = render_lines(&composite.display_lines(/*width*/ 80));
|
|
let permissions_line = rendered_lines
|
|
.iter()
|
|
.find(|line| line.contains("Permissions:"))
|
|
.expect("permissions line");
|
|
let permissions_text = permissions_line
|
|
.split("Permissions:")
|
|
.nth(1)
|
|
.map(str::trim)
|
|
.map(|text| text.trim_end_matches('│'))
|
|
.map(str::trim);
|
|
|
|
assert_eq!(
|
|
permissions_text,
|
|
Some("Custom (workspace-write with network access, on-request)")
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn status_snapshot_includes_forked_from() {
|
|
let temp_home = TempDir::new().expect("temp home");
|
|
let mut config = test_config(&temp_home).await;
|
|
config.model = Some("gpt-5.1-codex-max".to_string());
|
|
config.model_provider_id = "openai".to_string();
|
|
config.cwd = PathBuf::from("/workspace/tests").abs();
|
|
|
|
let account_display = test_status_account_display();
|
|
let usage = TokenUsage {
|
|
input_tokens: 800,
|
|
cached_input_tokens: 0,
|
|
output_tokens: 400,
|
|
reasoning_output_tokens: 0,
|
|
total_tokens: 1_200,
|
|
};
|
|
|
|
let captured_at = chrono::Local
|
|
.with_ymd_and_hms(2024, 8, 9, 10, 11, 12)
|
|
.single()
|
|
.expect("valid time");
|
|
|
|
let model_slug = crate::legacy_core::test_support::get_model_offline(config.model.as_deref());
|
|
let token_info = token_info_for(&model_slug, &config, &usage);
|
|
let session_id =
|
|
ThreadId::from_string("0f0f3c13-6cf9-4aa4-8b80-7d49c2f1be2e").expect("session id");
|
|
let forked_from =
|
|
ThreadId::from_string("e9f18a88-8081-4e51-9d4e-8af5cde2d8dd").expect("forked id");
|
|
|
|
let composite = new_status_output(
|
|
&config,
|
|
account_display.as_ref(),
|
|
Some(&token_info),
|
|
&usage,
|
|
&Some(session_id),
|
|
/*thread_name*/ None,
|
|
Some(forked_from),
|
|
/*rate_limits*/ None,
|
|
None,
|
|
captured_at,
|
|
&model_slug,
|
|
/*collaboration_mode*/ None,
|
|
/*reasoning_effort_override*/ None,
|
|
);
|
|
let mut rendered_lines = render_lines(&composite.display_lines(/*width*/ 80));
|
|
if cfg!(windows) {
|
|
for line in &mut rendered_lines {
|
|
*line = line.replace('\\', "/");
|
|
}
|
|
}
|
|
let sanitized = sanitize_directory(rendered_lines).join("\n");
|
|
assert_snapshot!(sanitized);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn status_snapshot_includes_monthly_limit() {
|
|
let temp_home = TempDir::new().expect("temp home");
|
|
let mut config = test_config(&temp_home).await;
|
|
config.model = Some("gpt-5.1-codex-max".to_string());
|
|
config.model_provider_id = "openai".to_string();
|
|
config.cwd = PathBuf::from("/workspace/tests").abs();
|
|
|
|
let account_display = test_status_account_display();
|
|
let usage = TokenUsage {
|
|
input_tokens: 800,
|
|
cached_input_tokens: 0,
|
|
output_tokens: 400,
|
|
reasoning_output_tokens: 0,
|
|
total_tokens: 1_200,
|
|
};
|
|
|
|
let captured_at = chrono::Local
|
|
.with_ymd_and_hms(2024, 5, 6, 7, 8, 9)
|
|
.single()
|
|
.expect("timestamp");
|
|
let snapshot = RateLimitSnapshot {
|
|
limit_id: None,
|
|
limit_name: None,
|
|
primary: Some(RateLimitWindow {
|
|
used_percent: 12.0,
|
|
window_minutes: Some(43_200),
|
|
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 86_400)),
|
|
}),
|
|
secondary: None,
|
|
credits: None,
|
|
plan_type: None,
|
|
};
|
|
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
|
|
|
let model_slug = crate::legacy_core::test_support::get_model_offline(config.model.as_deref());
|
|
let token_info = token_info_for(&model_slug, &config, &usage);
|
|
let composite = new_status_output(
|
|
&config,
|
|
account_display.as_ref(),
|
|
Some(&token_info),
|
|
&usage,
|
|
&None,
|
|
/*thread_name*/ None,
|
|
/*forked_from*/ None,
|
|
Some(&rate_display),
|
|
None,
|
|
captured_at,
|
|
&model_slug,
|
|
/*collaboration_mode*/ None,
|
|
/*reasoning_effort_override*/ None,
|
|
);
|
|
let mut rendered_lines = render_lines(&composite.display_lines(/*width*/ 80));
|
|
if cfg!(windows) {
|
|
for line in &mut rendered_lines {
|
|
*line = line.replace('\\', "/");
|
|
}
|
|
}
|
|
let sanitized = sanitize_directory(rendered_lines).join("\n");
|
|
assert_snapshot!(sanitized);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn status_snapshot_shows_unlimited_credits() {
|
|
let temp_home = TempDir::new().expect("temp home");
|
|
let config = test_config(&temp_home).await;
|
|
let account_display = test_status_account_display();
|
|
let usage = TokenUsage::default();
|
|
let captured_at = chrono::Local
|
|
.with_ymd_and_hms(2024, 2, 3, 4, 5, 6)
|
|
.single()
|
|
.expect("timestamp");
|
|
let snapshot = RateLimitSnapshot {
|
|
limit_id: None,
|
|
limit_name: None,
|
|
primary: None,
|
|
secondary: None,
|
|
credits: Some(CreditsSnapshot {
|
|
has_credits: true,
|
|
unlimited: true,
|
|
balance: None,
|
|
}),
|
|
plan_type: None,
|
|
};
|
|
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
|
let model_slug = crate::legacy_core::test_support::get_model_offline(config.model.as_deref());
|
|
let token_info = token_info_for(&model_slug, &config, &usage);
|
|
let composite = new_status_output(
|
|
&config,
|
|
account_display.as_ref(),
|
|
Some(&token_info),
|
|
&usage,
|
|
&None,
|
|
/*thread_name*/ None,
|
|
/*forked_from*/ None,
|
|
Some(&rate_display),
|
|
None,
|
|
captured_at,
|
|
&model_slug,
|
|
/*collaboration_mode*/ None,
|
|
/*reasoning_effort_override*/ None,
|
|
);
|
|
let rendered = render_lines(&composite.display_lines(/*width*/ 120));
|
|
assert!(
|
|
rendered
|
|
.iter()
|
|
.any(|line| line.contains("Credits:") && line.contains("Unlimited")),
|
|
"expected Credits: Unlimited line, got {rendered:?}"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn status_snapshot_shows_positive_credits() {
|
|
let temp_home = TempDir::new().expect("temp home");
|
|
let config = test_config(&temp_home).await;
|
|
let account_display = test_status_account_display();
|
|
let usage = TokenUsage::default();
|
|
let captured_at = chrono::Local
|
|
.with_ymd_and_hms(2024, 3, 4, 5, 6, 7)
|
|
.single()
|
|
.expect("timestamp");
|
|
let snapshot = RateLimitSnapshot {
|
|
limit_id: None,
|
|
limit_name: None,
|
|
primary: None,
|
|
secondary: None,
|
|
credits: Some(CreditsSnapshot {
|
|
has_credits: true,
|
|
unlimited: false,
|
|
balance: Some("12.5".to_string()),
|
|
}),
|
|
plan_type: None,
|
|
};
|
|
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
|
let model_slug = crate::legacy_core::test_support::get_model_offline(config.model.as_deref());
|
|
let token_info = token_info_for(&model_slug, &config, &usage);
|
|
let composite = new_status_output(
|
|
&config,
|
|
account_display.as_ref(),
|
|
Some(&token_info),
|
|
&usage,
|
|
&None,
|
|
/*thread_name*/ None,
|
|
/*forked_from*/ None,
|
|
Some(&rate_display),
|
|
None,
|
|
captured_at,
|
|
&model_slug,
|
|
/*collaboration_mode*/ None,
|
|
/*reasoning_effort_override*/ None,
|
|
);
|
|
let rendered = render_lines(&composite.display_lines(/*width*/ 120));
|
|
assert!(
|
|
rendered
|
|
.iter()
|
|
.any(|line| line.contains("Credits:") && line.contains("13 credits")),
|
|
"expected Credits line with rounded credits, got {rendered:?}"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn status_snapshot_hides_zero_credits() {
|
|
let temp_home = TempDir::new().expect("temp home");
|
|
let config = test_config(&temp_home).await;
|
|
let account_display = test_status_account_display();
|
|
let usage = TokenUsage::default();
|
|
let captured_at = chrono::Local
|
|
.with_ymd_and_hms(2024, 4, 5, 6, 7, 8)
|
|
.single()
|
|
.expect("timestamp");
|
|
let snapshot = RateLimitSnapshot {
|
|
limit_id: None,
|
|
limit_name: None,
|
|
primary: None,
|
|
secondary: None,
|
|
credits: Some(CreditsSnapshot {
|
|
has_credits: true,
|
|
unlimited: false,
|
|
balance: Some("0".to_string()),
|
|
}),
|
|
plan_type: None,
|
|
};
|
|
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
|
let model_slug = crate::legacy_core::test_support::get_model_offline(config.model.as_deref());
|
|
let token_info = token_info_for(&model_slug, &config, &usage);
|
|
let composite = new_status_output(
|
|
&config,
|
|
account_display.as_ref(),
|
|
Some(&token_info),
|
|
&usage,
|
|
&None,
|
|
/*thread_name*/ None,
|
|
/*forked_from*/ None,
|
|
Some(&rate_display),
|
|
None,
|
|
captured_at,
|
|
&model_slug,
|
|
/*collaboration_mode*/ None,
|
|
/*reasoning_effort_override*/ None,
|
|
);
|
|
let rendered = render_lines(&composite.display_lines(/*width*/ 120));
|
|
assert!(
|
|
rendered.iter().all(|line| !line.contains("Credits:")),
|
|
"expected no Credits line, got {rendered:?}"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn status_snapshot_hides_when_has_no_credits_flag() {
|
|
let temp_home = TempDir::new().expect("temp home");
|
|
let config = test_config(&temp_home).await;
|
|
let account_display = test_status_account_display();
|
|
let usage = TokenUsage::default();
|
|
let captured_at = chrono::Local
|
|
.with_ymd_and_hms(2024, 5, 6, 7, 8, 9)
|
|
.single()
|
|
.expect("timestamp");
|
|
let snapshot = RateLimitSnapshot {
|
|
limit_id: None,
|
|
limit_name: None,
|
|
primary: None,
|
|
secondary: None,
|
|
credits: Some(CreditsSnapshot {
|
|
has_credits: false,
|
|
unlimited: true,
|
|
balance: None,
|
|
}),
|
|
plan_type: None,
|
|
};
|
|
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
|
let model_slug = crate::legacy_core::test_support::get_model_offline(config.model.as_deref());
|
|
let token_info = token_info_for(&model_slug, &config, &usage);
|
|
let composite = new_status_output(
|
|
&config,
|
|
account_display.as_ref(),
|
|
Some(&token_info),
|
|
&usage,
|
|
&None,
|
|
/*thread_name*/ None,
|
|
/*forked_from*/ None,
|
|
Some(&rate_display),
|
|
None,
|
|
captured_at,
|
|
&model_slug,
|
|
/*collaboration_mode*/ None,
|
|
/*reasoning_effort_override*/ None,
|
|
);
|
|
let rendered = render_lines(&composite.display_lines(/*width*/ 120));
|
|
assert!(
|
|
rendered.iter().all(|line| !line.contains("Credits:")),
|
|
"expected no Credits line when has_credits is false, got {rendered:?}"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn status_card_token_usage_excludes_cached_tokens() {
|
|
let temp_home = TempDir::new().expect("temp home");
|
|
let mut config = test_config(&temp_home).await;
|
|
config.model = Some("gpt-5.1-codex-max".to_string());
|
|
config.cwd = PathBuf::from("/workspace/tests").abs();
|
|
|
|
let account_display = test_status_account_display();
|
|
let usage = TokenUsage {
|
|
input_tokens: 1_200,
|
|
cached_input_tokens: 200,
|
|
output_tokens: 900,
|
|
reasoning_output_tokens: 0,
|
|
total_tokens: 2_100,
|
|
};
|
|
|
|
let now = chrono::Local
|
|
.with_ymd_and_hms(2024, 1, 1, 0, 0, 0)
|
|
.single()
|
|
.expect("timestamp");
|
|
|
|
let model_slug = crate::legacy_core::test_support::get_model_offline(config.model.as_deref());
|
|
let token_info = token_info_for(&model_slug, &config, &usage);
|
|
let composite = new_status_output(
|
|
&config,
|
|
account_display.as_ref(),
|
|
Some(&token_info),
|
|
&usage,
|
|
&None,
|
|
/*thread_name*/ None,
|
|
/*forked_from*/ None,
|
|
/*rate_limits*/ None,
|
|
None,
|
|
now,
|
|
&model_slug,
|
|
/*collaboration_mode*/ None,
|
|
/*reasoning_effort_override*/ None,
|
|
);
|
|
let rendered = render_lines(&composite.display_lines(/*width*/ 120));
|
|
|
|
assert!(
|
|
rendered.iter().all(|line| !line.contains("cached")),
|
|
"cached tokens should not be displayed, got: {rendered:?}"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn status_snapshot_truncates_in_narrow_terminal() {
|
|
let temp_home = TempDir::new().expect("temp home");
|
|
let mut config = test_config(&temp_home).await;
|
|
config.model = Some("gpt-5.1-codex-max".to_string());
|
|
config.model_provider_id = "openai".to_string();
|
|
config.model_reasoning_summary = Some(ReasoningSummary::Detailed);
|
|
config.cwd = PathBuf::from("/workspace/tests").abs();
|
|
|
|
let account_display = test_status_account_display();
|
|
let usage = TokenUsage {
|
|
input_tokens: 1_200,
|
|
cached_input_tokens: 200,
|
|
output_tokens: 900,
|
|
reasoning_output_tokens: 150,
|
|
total_tokens: 2_250,
|
|
};
|
|
|
|
let captured_at = chrono::Local
|
|
.with_ymd_and_hms(2024, 1, 2, 3, 4, 5)
|
|
.single()
|
|
.expect("timestamp");
|
|
let snapshot = RateLimitSnapshot {
|
|
limit_id: None,
|
|
limit_name: None,
|
|
primary: Some(RateLimitWindow {
|
|
used_percent: 72.5,
|
|
window_minutes: Some(300),
|
|
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 600)),
|
|
}),
|
|
secondary: None,
|
|
credits: None,
|
|
plan_type: None,
|
|
};
|
|
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
|
|
|
let model_slug = crate::legacy_core::test_support::get_model_offline(config.model.as_deref());
|
|
let token_info = token_info_for(&model_slug, &config, &usage);
|
|
let reasoning_effort_override = Some(Some(ReasoningEffort::High));
|
|
let composite = new_status_output(
|
|
&config,
|
|
account_display.as_ref(),
|
|
Some(&token_info),
|
|
&usage,
|
|
&None,
|
|
/*thread_name*/ None,
|
|
/*forked_from*/ None,
|
|
Some(&rate_display),
|
|
None,
|
|
captured_at,
|
|
&model_slug,
|
|
/*collaboration_mode*/ None,
|
|
reasoning_effort_override,
|
|
);
|
|
let mut rendered_lines = render_lines(&composite.display_lines(/*width*/ 70));
|
|
if cfg!(windows) {
|
|
for line in &mut rendered_lines {
|
|
*line = line.replace('\\', "/");
|
|
}
|
|
}
|
|
let sanitized = sanitize_directory(rendered_lines).join("\n");
|
|
|
|
assert_snapshot!(sanitized);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn status_snapshot_shows_missing_limits_message() {
|
|
let temp_home = TempDir::new().expect("temp home");
|
|
let mut config = test_config(&temp_home).await;
|
|
config.model = Some("gpt-5.1-codex-max".to_string());
|
|
config.cwd = PathBuf::from("/workspace/tests").abs();
|
|
|
|
let account_display = test_status_account_display();
|
|
let usage = TokenUsage {
|
|
input_tokens: 500,
|
|
cached_input_tokens: 0,
|
|
output_tokens: 250,
|
|
reasoning_output_tokens: 0,
|
|
total_tokens: 750,
|
|
};
|
|
|
|
let now = chrono::Local
|
|
.with_ymd_and_hms(2024, 2, 3, 4, 5, 6)
|
|
.single()
|
|
.expect("timestamp");
|
|
|
|
let model_slug = crate::legacy_core::test_support::get_model_offline(config.model.as_deref());
|
|
let token_info = token_info_for(&model_slug, &config, &usage);
|
|
let composite = new_status_output(
|
|
&config,
|
|
account_display.as_ref(),
|
|
Some(&token_info),
|
|
&usage,
|
|
&None,
|
|
/*thread_name*/ None,
|
|
/*forked_from*/ None,
|
|
/*rate_limits*/ None,
|
|
None,
|
|
now,
|
|
&model_slug,
|
|
/*collaboration_mode*/ None,
|
|
/*reasoning_effort_override*/ None,
|
|
);
|
|
let mut rendered_lines = render_lines(&composite.display_lines(/*width*/ 80));
|
|
if cfg!(windows) {
|
|
for line in &mut rendered_lines {
|
|
*line = line.replace('\\', "/");
|
|
}
|
|
}
|
|
let sanitized = sanitize_directory(rendered_lines).join("\n");
|
|
assert_snapshot!(sanitized);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn status_snapshot_shows_refreshing_limits_notice() {
|
|
let temp_home = TempDir::new().expect("temp home");
|
|
let mut config = test_config(&temp_home).await;
|
|
config.model = Some("gpt-5.1-codex-max".to_string());
|
|
config.cwd = PathBuf::from("/workspace/tests").abs();
|
|
|
|
let usage = TokenUsage {
|
|
input_tokens: 500,
|
|
cached_input_tokens: 0,
|
|
output_tokens: 250,
|
|
reasoning_output_tokens: 0,
|
|
total_tokens: 750,
|
|
};
|
|
let captured_at = chrono::Local
|
|
.with_ymd_and_hms(2024, 6, 7, 8, 9, 10)
|
|
.single()
|
|
.expect("timestamp");
|
|
let snapshot = RateLimitSnapshot {
|
|
limit_id: None,
|
|
limit_name: None,
|
|
primary: Some(RateLimitWindow {
|
|
used_percent: 45.0,
|
|
window_minutes: Some(300),
|
|
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 900)),
|
|
}),
|
|
secondary: Some(RateLimitWindow {
|
|
used_percent: 30.0,
|
|
window_minutes: Some(10_080),
|
|
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 2_700)),
|
|
}),
|
|
credits: None,
|
|
plan_type: None,
|
|
};
|
|
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
|
|
|
let model_slug = crate::legacy_core::test_support::get_model_offline(config.model.as_deref());
|
|
let token_info = token_info_for(&model_slug, &config, &usage);
|
|
let composite = new_status_output_with_rate_limits(
|
|
&config,
|
|
/*account_display*/ None,
|
|
Some(&token_info),
|
|
&usage,
|
|
&None,
|
|
/*thread_name*/ None,
|
|
/*forked_from*/ None,
|
|
std::slice::from_ref(&rate_display),
|
|
None,
|
|
captured_at,
|
|
&model_slug,
|
|
/*collaboration_mode*/ None,
|
|
/*reasoning_effort_override*/ None,
|
|
/*refreshing_rate_limits*/ true,
|
|
);
|
|
let mut rendered_lines = render_lines(&composite.display_lines(/*width*/ 80));
|
|
if cfg!(windows) {
|
|
for line in &mut rendered_lines {
|
|
*line = line.replace('\\', "/");
|
|
}
|
|
}
|
|
let sanitized = sanitize_directory(rendered_lines).join("\n");
|
|
assert_snapshot!(sanitized);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn status_snapshot_includes_credits_and_limits() {
|
|
let temp_home = TempDir::new().expect("temp home");
|
|
let mut config = test_config(&temp_home).await;
|
|
config.model = Some("gpt-5.1-codex".to_string());
|
|
config.cwd = PathBuf::from("/workspace/tests").abs();
|
|
|
|
let account_display = test_status_account_display();
|
|
let usage = TokenUsage {
|
|
input_tokens: 1_500,
|
|
cached_input_tokens: 100,
|
|
output_tokens: 600,
|
|
reasoning_output_tokens: 0,
|
|
total_tokens: 2_200,
|
|
};
|
|
|
|
let captured_at = chrono::Local
|
|
.with_ymd_and_hms(2024, 7, 8, 9, 10, 11)
|
|
.single()
|
|
.expect("timestamp");
|
|
let snapshot = RateLimitSnapshot {
|
|
limit_id: None,
|
|
limit_name: None,
|
|
primary: Some(RateLimitWindow {
|
|
used_percent: 45.0,
|
|
window_minutes: Some(300),
|
|
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 900)),
|
|
}),
|
|
secondary: Some(RateLimitWindow {
|
|
used_percent: 30.0,
|
|
window_minutes: Some(10_080),
|
|
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 2_700)),
|
|
}),
|
|
credits: Some(CreditsSnapshot {
|
|
has_credits: true,
|
|
unlimited: false,
|
|
balance: Some("37.5".to_string()),
|
|
}),
|
|
plan_type: None,
|
|
};
|
|
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
|
|
|
let model_slug = crate::legacy_core::test_support::get_model_offline(config.model.as_deref());
|
|
let token_info = token_info_for(&model_slug, &config, &usage);
|
|
let composite = new_status_output(
|
|
&config,
|
|
account_display.as_ref(),
|
|
Some(&token_info),
|
|
&usage,
|
|
&None,
|
|
/*thread_name*/ None,
|
|
/*forked_from*/ None,
|
|
Some(&rate_display),
|
|
None,
|
|
captured_at,
|
|
&model_slug,
|
|
/*collaboration_mode*/ None,
|
|
/*reasoning_effort_override*/ None,
|
|
);
|
|
let mut rendered_lines = render_lines(&composite.display_lines(/*width*/ 80));
|
|
if cfg!(windows) {
|
|
for line in &mut rendered_lines {
|
|
*line = line.replace('\\', "/");
|
|
}
|
|
}
|
|
let sanitized = sanitize_directory(rendered_lines).join("\n");
|
|
assert_snapshot!(sanitized);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn status_snapshot_shows_unavailable_limits_message() {
|
|
let temp_home = TempDir::new().expect("temp home");
|
|
let mut config = test_config(&temp_home).await;
|
|
config.model = Some("gpt-5.1-codex-max".to_string());
|
|
config.cwd = PathBuf::from("/workspace/tests").abs();
|
|
|
|
let account_display = test_status_account_display();
|
|
let usage = TokenUsage {
|
|
input_tokens: 500,
|
|
cached_input_tokens: 0,
|
|
output_tokens: 250,
|
|
reasoning_output_tokens: 0,
|
|
total_tokens: 750,
|
|
};
|
|
|
|
let snapshot = RateLimitSnapshot {
|
|
limit_id: None,
|
|
limit_name: None,
|
|
primary: None,
|
|
secondary: None,
|
|
credits: None,
|
|
plan_type: None,
|
|
};
|
|
let captured_at = chrono::Local
|
|
.with_ymd_and_hms(2024, 6, 7, 8, 9, 10)
|
|
.single()
|
|
.expect("timestamp");
|
|
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
|
|
|
let model_slug = crate::legacy_core::test_support::get_model_offline(config.model.as_deref());
|
|
let token_info = token_info_for(&model_slug, &config, &usage);
|
|
let composite = new_status_output(
|
|
&config,
|
|
account_display.as_ref(),
|
|
Some(&token_info),
|
|
&usage,
|
|
&None,
|
|
/*thread_name*/ None,
|
|
/*forked_from*/ None,
|
|
Some(&rate_display),
|
|
None,
|
|
captured_at,
|
|
&model_slug,
|
|
/*collaboration_mode*/ None,
|
|
/*reasoning_effort_override*/ None,
|
|
);
|
|
let mut rendered_lines = render_lines(&composite.display_lines(/*width*/ 80));
|
|
if cfg!(windows) {
|
|
for line in &mut rendered_lines {
|
|
*line = line.replace('\\', "/");
|
|
}
|
|
}
|
|
let sanitized = sanitize_directory(rendered_lines).join("\n");
|
|
assert_snapshot!(sanitized);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn status_snapshot_treats_refreshing_empty_limits_as_unavailable() {
|
|
let temp_home = TempDir::new().expect("temp home");
|
|
let mut config = test_config(&temp_home).await;
|
|
config.model = Some("gpt-5.1-codex-max".to_string());
|
|
config.cwd = PathBuf::from("/workspace/tests").abs();
|
|
|
|
let usage = TokenUsage {
|
|
input_tokens: 500,
|
|
cached_input_tokens: 0,
|
|
output_tokens: 250,
|
|
reasoning_output_tokens: 0,
|
|
total_tokens: 750,
|
|
};
|
|
|
|
let snapshot = RateLimitSnapshot {
|
|
limit_id: None,
|
|
limit_name: None,
|
|
primary: None,
|
|
secondary: None,
|
|
credits: None,
|
|
plan_type: None,
|
|
};
|
|
let captured_at = chrono::Local
|
|
.with_ymd_and_hms(2024, 6, 7, 8, 9, 10)
|
|
.single()
|
|
.expect("timestamp");
|
|
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
|
|
|
let model_slug = crate::legacy_core::test_support::get_model_offline(config.model.as_deref());
|
|
let token_info = token_info_for(&model_slug, &config, &usage);
|
|
let composite = new_status_output_with_rate_limits(
|
|
&config,
|
|
/*account_display*/ None,
|
|
Some(&token_info),
|
|
&usage,
|
|
&None,
|
|
/*thread_name*/ None,
|
|
/*forked_from*/ None,
|
|
std::slice::from_ref(&rate_display),
|
|
None,
|
|
captured_at,
|
|
&model_slug,
|
|
/*collaboration_mode*/ None,
|
|
/*reasoning_effort_override*/ None,
|
|
/*refreshing_rate_limits*/ true,
|
|
);
|
|
let mut rendered_lines = render_lines(&composite.display_lines(/*width*/ 80));
|
|
if cfg!(windows) {
|
|
for line in &mut rendered_lines {
|
|
*line = line.replace('\\', "/");
|
|
}
|
|
}
|
|
let sanitized = sanitize_directory(rendered_lines).join("\n");
|
|
assert_snapshot!(sanitized);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn status_snapshot_shows_stale_limits_message() {
|
|
let temp_home = TempDir::new().expect("temp home");
|
|
let mut config = test_config(&temp_home).await;
|
|
config.model = Some("gpt-5.1-codex-max".to_string());
|
|
config.cwd = PathBuf::from("/workspace/tests").abs();
|
|
|
|
let account_display = test_status_account_display();
|
|
let usage = TokenUsage {
|
|
input_tokens: 1_200,
|
|
cached_input_tokens: 200,
|
|
output_tokens: 900,
|
|
reasoning_output_tokens: 150,
|
|
total_tokens: 2_250,
|
|
};
|
|
|
|
let captured_at = chrono::Local
|
|
.with_ymd_and_hms(2024, 1, 2, 3, 4, 5)
|
|
.single()
|
|
.expect("timestamp");
|
|
let snapshot = RateLimitSnapshot {
|
|
limit_id: None,
|
|
limit_name: None,
|
|
primary: Some(RateLimitWindow {
|
|
used_percent: 72.5,
|
|
window_minutes: Some(300),
|
|
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 600)),
|
|
}),
|
|
secondary: Some(RateLimitWindow {
|
|
used_percent: 40.0,
|
|
window_minutes: Some(10_080),
|
|
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 1_800)),
|
|
}),
|
|
credits: None,
|
|
plan_type: None,
|
|
};
|
|
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
|
let now = captured_at + ChronoDuration::minutes(20);
|
|
|
|
let model_slug = crate::legacy_core::test_support::get_model_offline(config.model.as_deref());
|
|
let token_info = token_info_for(&model_slug, &config, &usage);
|
|
let composite = new_status_output(
|
|
&config,
|
|
account_display.as_ref(),
|
|
Some(&token_info),
|
|
&usage,
|
|
&None,
|
|
/*thread_name*/ None,
|
|
/*forked_from*/ None,
|
|
Some(&rate_display),
|
|
None,
|
|
now,
|
|
&model_slug,
|
|
/*collaboration_mode*/ None,
|
|
/*reasoning_effort_override*/ None,
|
|
);
|
|
let mut rendered_lines = render_lines(&composite.display_lines(/*width*/ 80));
|
|
if cfg!(windows) {
|
|
for line in &mut rendered_lines {
|
|
*line = line.replace('\\', "/");
|
|
}
|
|
}
|
|
let sanitized = sanitize_directory(rendered_lines).join("\n");
|
|
assert_snapshot!(sanitized);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn status_snapshot_cached_limits_hide_credits_without_flag() {
|
|
let temp_home = TempDir::new().expect("temp home");
|
|
let mut config = test_config(&temp_home).await;
|
|
config.model = Some("gpt-5.1-codex".to_string());
|
|
config.cwd = PathBuf::from("/workspace/tests").abs();
|
|
|
|
let account_display = test_status_account_display();
|
|
let usage = TokenUsage {
|
|
input_tokens: 900,
|
|
cached_input_tokens: 200,
|
|
output_tokens: 350,
|
|
reasoning_output_tokens: 0,
|
|
total_tokens: 1_450,
|
|
};
|
|
|
|
let captured_at = chrono::Local
|
|
.with_ymd_and_hms(2024, 9, 10, 11, 12, 13)
|
|
.single()
|
|
.expect("timestamp");
|
|
let snapshot = RateLimitSnapshot {
|
|
limit_id: None,
|
|
limit_name: None,
|
|
primary: Some(RateLimitWindow {
|
|
used_percent: 60.0,
|
|
window_minutes: Some(300),
|
|
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 1_200)),
|
|
}),
|
|
secondary: Some(RateLimitWindow {
|
|
used_percent: 35.0,
|
|
window_minutes: Some(10_080),
|
|
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 2_400)),
|
|
}),
|
|
credits: Some(CreditsSnapshot {
|
|
has_credits: false,
|
|
unlimited: false,
|
|
balance: Some("80".to_string()),
|
|
}),
|
|
plan_type: None,
|
|
};
|
|
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
|
let now = captured_at + ChronoDuration::minutes(20);
|
|
|
|
let model_slug = crate::legacy_core::test_support::get_model_offline(config.model.as_deref());
|
|
let token_info = token_info_for(&model_slug, &config, &usage);
|
|
let composite = new_status_output(
|
|
&config,
|
|
account_display.as_ref(),
|
|
Some(&token_info),
|
|
&usage,
|
|
&None,
|
|
/*thread_name*/ None,
|
|
/*forked_from*/ None,
|
|
Some(&rate_display),
|
|
None,
|
|
now,
|
|
&model_slug,
|
|
/*collaboration_mode*/ None,
|
|
/*reasoning_effort_override*/ None,
|
|
);
|
|
let mut rendered_lines = render_lines(&composite.display_lines(/*width*/ 80));
|
|
if cfg!(windows) {
|
|
for line in &mut rendered_lines {
|
|
*line = line.replace('\\', "/");
|
|
}
|
|
}
|
|
let sanitized = sanitize_directory(rendered_lines).join("\n");
|
|
assert_snapshot!(sanitized);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn status_context_window_uses_last_usage() {
|
|
let temp_home = TempDir::new().expect("temp home");
|
|
let mut config = test_config(&temp_home).await;
|
|
config.model_context_window = Some(272_000);
|
|
|
|
let account_display = test_status_account_display();
|
|
let total_usage = TokenUsage {
|
|
input_tokens: 12_800,
|
|
cached_input_tokens: 0,
|
|
output_tokens: 879,
|
|
reasoning_output_tokens: 0,
|
|
total_tokens: 102_000,
|
|
};
|
|
let last_usage = TokenUsage {
|
|
input_tokens: 12_800,
|
|
cached_input_tokens: 0,
|
|
output_tokens: 879,
|
|
reasoning_output_tokens: 0,
|
|
total_tokens: 13_679,
|
|
};
|
|
|
|
let now = chrono::Local
|
|
.with_ymd_and_hms(2024, 6, 1, 12, 0, 0)
|
|
.single()
|
|
.expect("timestamp");
|
|
|
|
let model_slug = crate::legacy_core::test_support::get_model_offline(config.model.as_deref());
|
|
let token_info = TokenUsageInfo {
|
|
total_token_usage: total_usage.clone(),
|
|
last_token_usage: last_usage,
|
|
model_context_window: config.model_context_window,
|
|
};
|
|
let composite = new_status_output(
|
|
&config,
|
|
account_display.as_ref(),
|
|
Some(&token_info),
|
|
&total_usage,
|
|
&None,
|
|
/*thread_name*/ None,
|
|
/*forked_from*/ None,
|
|
/*rate_limits*/ None,
|
|
None,
|
|
now,
|
|
&model_slug,
|
|
/*collaboration_mode*/ None,
|
|
/*reasoning_effort_override*/ None,
|
|
);
|
|
let rendered_lines = render_lines(&composite.display_lines(/*width*/ 80));
|
|
let context_line = rendered_lines
|
|
.into_iter()
|
|
.find(|line| line.contains("Context window"))
|
|
.expect("context line");
|
|
|
|
assert!(
|
|
context_line.contains("13.7K used / 272K"),
|
|
"expected context line to reflect last usage tokens, got: {context_line}"
|
|
);
|
|
assert!(
|
|
!context_line.contains("102K"),
|
|
"context line should not use total aggregated tokens, got: {context_line}"
|
|
);
|
|
}
|