mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
c8e5db16c9
## Summary
Enterprise users can have an effective monthly credit limit, but Codex
`/status` currently drops that metadata from the account-usage response.
This change adds the optional `spend_control.individual_limit`
projection to the existing rate-limit snapshot flow. The backend client
reads the monthly limit, app-server exposes it as `individualLimit`, and
the TUI renders a `Monthly credit limit` row through the existing
progress-bar renderer.
When the backend does not return an effective monthly limit, existing
rate-limit behavior is unchanged.
## Existing backend state
The account-usage backend already returns the effective monthly limit
and current usage together:
```json
{
"spend_control": {
"reached": false,
"individual_limit": {
"limit": "25000",
"used": "8000",
"remaining": "17000",
"used_percent": 32,
"remaining_percent": 68,
"reset_after_seconds": 86400,
"reset_at": 1778137680
}
}
}
```
Before this change, Codex projected rolling `primary` and `secondary`
windows plus `credits`. It ignored `spend_control.individual_limit`, so
app-server clients and `/status` could not render the monthly cap.
The updated flow is:
```text
account usage backend
-> backend-client reads spend_control.individual_limit
-> existing rate-limit snapshot carries optional individual_limit
-> app-server exposes optional individualLimit
-> TUI renders Monthly credit limit
```
## App-server contract
`account/rateLimits/read` and sparse `account/rateLimits/updated`
notifications now include an additive nullable
`rateLimits.individualLimit` field:
```json
{
"individualLimit": {
"limit": "25000",
"used": "8000",
"remainingPercent": 68,
"resetsAt": 1778137680
}
}
```
In an `account/rateLimits/read` response, `null` means no monthly limit
is available. `account/rateLimits/updated` remains a sparse rolling
notification: clients merge available values into their most recent
`account/rateLimits/read` snapshot or refetch. Nullable account metadata
in a rolling notification does not clear a previously observed value.
## Design decisions
- Extend the existing rate-limit snapshot instead of introducing a
separate request or wire-level update protocol.
- Keep the Codex projection narrow: `/status` needs the effective limit,
current usage, remaining percentage, and reset timestamp.
- Render the monthly row through the existing progress-bar renderer,
with one optional detail line for `8,000 of 25,000 credits used`.
- Keep the backend response optional so existing accounts and older
usage states preserve their current behavior.
- Preserve cached monthly metadata when sparse rolling notifications
omit it. Live account-usage reads remain authoritative and can clear a
removed limit.
## Visual evidence
```text
Monthly credit limit: [██████████████░░░░░░] 68% left (resets 07:08 on 7 May)
8,000 of 25,000 credits used
```
Snapshot:
`codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_enterprise_monthly_credit_limit.snap`
## Testing
Tests: generated app-server schema verification, protocol tests,
backend-client tests, app-server integration coverage, TUI snapshot
coverage, formatting, and workspace lint cleanup.
1972 lines
66 KiB
Rust
1972 lines
66 KiB
Rust
use super::new_status_output;
|
|
use super::new_status_output_with_rate_limits;
|
|
use super::new_status_output_with_rate_limits_handle;
|
|
use super::rate_limit_snapshot_display;
|
|
use super::rate_limits::RateLimitSnapshotDisplay;
|
|
use super::rate_limits::RateLimitWindowDisplay;
|
|
use super::rate_limits::SpendControlLimitSnapshotDisplay;
|
|
use super::rate_limits::StatusRateLimitData;
|
|
use super::rate_limits::compose_rate_limit_data_many;
|
|
use crate::history_cell::HistoryCell;
|
|
use crate::legacy_core::config::Config;
|
|
use crate::legacy_core::config::ConfigBuilder;
|
|
use crate::legacy_core::config::PermissionProfileSnapshot;
|
|
use crate::status::StatusAccountDisplay;
|
|
use crate::status::remote_connection::RemoteConnectionStatus;
|
|
use crate::test_support::PathBufExt;
|
|
use crate::test_support::test_path_buf;
|
|
use crate::token_usage::TokenUsage;
|
|
use crate::token_usage::TokenUsageInfo;
|
|
use chrono::Duration as ChronoDuration;
|
|
use chrono::Local;
|
|
use chrono::TimeZone;
|
|
use chrono::Utc;
|
|
use codex_app_server_protocol::AskForApproval;
|
|
use codex_app_server_protocol::CreditsSnapshot;
|
|
use codex_app_server_protocol::RateLimitSnapshot;
|
|
use codex_app_server_protocol::RateLimitWindow;
|
|
use codex_app_server_protocol::SpendControlLimitSnapshot;
|
|
use codex_config::LoaderOverrides;
|
|
use codex_model_provider_info::ModelProviderAwsAuthInfo;
|
|
use codex_model_provider_info::ModelProviderInfo;
|
|
use codex_protocol::ThreadId;
|
|
use codex_protocol::config_types::ApprovalsReviewer;
|
|
use codex_protocol::config_types::ReasoningSummary;
|
|
use codex_protocol::models::ActivePermissionProfile;
|
|
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
|
|
use codex_protocol::models::ManagedFileSystemPermissions;
|
|
use codex_protocol::models::PermissionProfile;
|
|
use codex_protocol::openai_models::ReasoningEffort;
|
|
use codex_protocol::permissions::FileSystemAccessMode;
|
|
use codex_protocol::permissions::FileSystemPath;
|
|
use codex_protocol::permissions::FileSystemSandboxEntry;
|
|
use codex_protocol::permissions::FileSystemSpecialPath;
|
|
use codex_protocol::permissions::NetworkSandboxPolicy;
|
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
use insta::assert_snapshot;
|
|
use pretty_assertions::assert_eq;
|
|
use ratatui::prelude::*;
|
|
use tempfile::TempDir;
|
|
use unicode_width::UnicodeWidthStr;
|
|
|
|
#[test]
|
|
fn stale_monthly_limit_marks_fresh_rolling_snapshot_stale() {
|
|
let now = Local::now();
|
|
let snapshot = RateLimitSnapshotDisplay {
|
|
limit_name: "codex".to_string(),
|
|
captured_at: now,
|
|
primary: Some(RateLimitWindowDisplay {
|
|
used_percent: 20.0,
|
|
resets_at: Some("soon".to_string()),
|
|
window_minutes: Some(300),
|
|
}),
|
|
secondary: None,
|
|
credits: None,
|
|
individual_limit: Some(SpendControlLimitSnapshotDisplay {
|
|
captured_at: now - ChronoDuration::minutes(20),
|
|
percent_remaining: 68.0,
|
|
used: "8,000".to_string(),
|
|
limit: "25,000".to_string(),
|
|
resets_at: Some("later".to_string()),
|
|
}),
|
|
};
|
|
|
|
assert!(matches!(
|
|
compose_rate_limit_data_many(&[snapshot], now),
|
|
StatusRateLimitData::Stale(_)
|
|
));
|
|
}
|
|
|
|
fn app_server_workspace_write_profile(network_enabled: bool) -> PermissionProfile {
|
|
PermissionProfile::Managed {
|
|
network: if network_enabled {
|
|
NetworkSandboxPolicy::Enabled
|
|
} else {
|
|
NetworkSandboxPolicy::Restricted
|
|
},
|
|
file_system: ManagedFileSystemPermissions::Restricted {
|
|
entries: vec![
|
|
FileSystemSandboxEntry {
|
|
path: FileSystemPath::Special {
|
|
value: FileSystemSpecialPath::Root,
|
|
},
|
|
access: FileSystemAccessMode::Read,
|
|
},
|
|
FileSystemSandboxEntry {
|
|
path: FileSystemPath::Special {
|
|
value: FileSystemSpecialPath::ProjectRoots { subpath: None },
|
|
},
|
|
access: FileSystemAccessMode::Write,
|
|
},
|
|
FileSystemSandboxEntry {
|
|
path: FileSystemPath::Special {
|
|
value: FileSystemSpecialPath::SlashTmp,
|
|
},
|
|
access: FileSystemAccessMode::Write,
|
|
},
|
|
FileSystemSandboxEntry {
|
|
path: FileSystemPath::Special {
|
|
value: FileSystemSpecialPath::Tmpdir,
|
|
},
|
|
access: FileSystemAccessMode::Write,
|
|
},
|
|
],
|
|
glob_scan_max_depth: None,
|
|
},
|
|
}
|
|
}
|
|
|
|
async fn test_config(temp_home: &TempDir) -> Config {
|
|
let mut config = ConfigBuilder::default()
|
|
.codex_home(temp_home.path().to_path_buf())
|
|
.loader_overrides(LoaderOverrides::without_managed_config_for_tests())
|
|
.build()
|
|
.await
|
|
.expect("load config");
|
|
config.approvals_reviewer = ApprovalsReviewer::User;
|
|
config
|
|
.permissions
|
|
.set_permission_profile(app_server_workspace_write_profile(
|
|
/*network_enabled*/ true,
|
|
))
|
|
.expect("set permission profile");
|
|
config
|
|
}
|
|
|
|
fn set_workspace_cwd(config: &mut Config, cwd: AbsolutePathBuf) {
|
|
config.cwd = cwd.clone();
|
|
config.workspace_roots = vec![cwd];
|
|
config
|
|
.permissions
|
|
.set_workspace_roots(config.workspace_roots.clone());
|
|
}
|
|
|
|
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> {
|
|
let frame_width = lines
|
|
.iter()
|
|
.find(|line| line.starts_with('╭'))
|
|
.map(|line| UnicodeWidthStr::width(line.as_str()));
|
|
lines
|
|
.into_iter()
|
|
.map(|line| {
|
|
if let (Some(frame_width), Some(dir_pos), Some(pipe_idx)) =
|
|
(frame_width, line.find("Directory: "), line.rfind('│'))
|
|
{
|
|
let prefix = &line[..dir_pos + "Directory: ".len()];
|
|
let suffix = &line[pipe_idx..];
|
|
let replacement = "[[workspace]]";
|
|
let content_width = frame_width.saturating_sub(
|
|
UnicodeWidthStr::width(prefix) + UnicodeWidthStr::width(suffix),
|
|
);
|
|
let mut rebuilt = prefix.to_string();
|
|
rebuilt.push_str(replacement);
|
|
let replacement_width = UnicodeWidthStr::width(replacement);
|
|
if content_width > replacement_width {
|
|
rebuilt.push_str(&" ".repeat(content_width - replacement_width));
|
|
}
|
|
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()
|
|
}
|
|
|
|
fn permissions_text_for(config: &Config) -> Option<String> {
|
|
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,
|
|
test_status_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,
|
|
);
|
|
render_lines(&composite.display_lines(/*width*/ 80))
|
|
.iter()
|
|
.find(|line| line.contains("Permissions:"))
|
|
.and_then(|line| {
|
|
line.split("Permissions:")
|
|
.nth(1)
|
|
.map(str::trim)
|
|
.map(|text| text.trim_end_matches('│'))
|
|
.map(str::trim)
|
|
.map(ToString::to_string)
|
|
})
|
|
}
|
|
|
|
#[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);
|
|
set_workspace_cwd(&mut config, test_path_buf("/workspace/tests").abs());
|
|
config
|
|
.permissions
|
|
.set_permission_profile(PermissionProfile::workspace_write())
|
|
.expect("set permission profile");
|
|
|
|
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,
|
|
window_duration_mins: Some(300),
|
|
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 600)),
|
|
}),
|
|
secondary: Some(RateLimitWindow {
|
|
used_percent: 45,
|
|
window_duration_mins: Some(10080),
|
|
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 1_200)),
|
|
}),
|
|
credits: None,
|
|
individual_limit: None,
|
|
plan_type: None,
|
|
rate_limit_reached_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_uses_workspace_label() {
|
|
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.to_core())
|
|
.expect("set approval policy");
|
|
set_workspace_cwd(&mut config, test_path_buf("/workspace/tests").abs());
|
|
config
|
|
.permissions
|
|
.set_permission_profile(app_server_workspace_write_profile(
|
|
/*network_enabled*/ true,
|
|
))
|
|
.expect("set permission profile");
|
|
|
|
assert_eq!(
|
|
permissions_text_for(&config).as_deref(),
|
|
Some("Custom (workspace with network access, Ask for approval)")
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn status_permissions_named_read_only_profile_shows_builtin_label() {
|
|
let temp_home = TempDir::new().expect("temp home");
|
|
let mut config = test_config(&temp_home).await;
|
|
config
|
|
.permissions
|
|
.approval_policy
|
|
.set(AskForApproval::OnRequest.to_core())
|
|
.expect("set approval policy");
|
|
config
|
|
.permissions
|
|
.set_permission_profile_from_session_snapshot(PermissionProfileSnapshot::active(
|
|
PermissionProfile::read_only(),
|
|
ActivePermissionProfile::read_only(),
|
|
))
|
|
.expect("set permission profile");
|
|
|
|
assert_eq!(
|
|
permissions_text_for(&config).as_deref(),
|
|
Some("Read Only (Ask for approval)")
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn status_permissions_read_only_profile_shows_additional_writable_roots() {
|
|
let temp_home = TempDir::new().expect("temp home");
|
|
let mut config = test_config(&temp_home).await;
|
|
config
|
|
.permissions
|
|
.approval_policy
|
|
.set(AskForApproval::OnRequest.to_core())
|
|
.expect("set approval policy");
|
|
let extra_root = test_path_buf("/workspace/extra").abs();
|
|
let file_system_policy = PermissionProfile::read_only()
|
|
.file_system_sandbox_policy()
|
|
.with_additional_writable_roots(config.cwd.as_path(), std::slice::from_ref(&extra_root));
|
|
config
|
|
.permissions
|
|
.set_permission_profile_from_session_snapshot(PermissionProfileSnapshot::active(
|
|
PermissionProfile::from_runtime_permissions(
|
|
&file_system_policy,
|
|
NetworkSandboxPolicy::Restricted,
|
|
),
|
|
ActivePermissionProfile::read_only(),
|
|
))
|
|
.expect("set permission profile");
|
|
|
|
assert_eq!(
|
|
permissions_text_for(&config).as_deref(),
|
|
Some("Read Only (Ask for approval)")
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn status_permissions_named_workspace_profile_shows_builtin_label() {
|
|
let temp_home = TempDir::new().expect("temp home");
|
|
let mut config = test_config(&temp_home).await;
|
|
config
|
|
.permissions
|
|
.approval_policy
|
|
.set(AskForApproval::OnRequest.to_core())
|
|
.expect("set approval policy");
|
|
config
|
|
.permissions
|
|
.set_permission_profile_from_session_snapshot(PermissionProfileSnapshot::active(
|
|
PermissionProfile::workspace_write(),
|
|
ActivePermissionProfile::new(BUILT_IN_PERMISSION_PROFILE_WORKSPACE),
|
|
))
|
|
.expect("set permission profile");
|
|
|
|
assert_eq!(
|
|
permissions_text_for(&config).as_deref(),
|
|
Some("Workspace (Ask for approval)")
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn status_permissions_workspace_auto_review_shows_reviewer_label() {
|
|
let temp_home = TempDir::new().expect("temp home");
|
|
let mut config = test_config(&temp_home).await;
|
|
config.approvals_reviewer = ApprovalsReviewer::AutoReview;
|
|
config
|
|
.permissions
|
|
.approval_policy
|
|
.set(AskForApproval::OnRequest.to_core())
|
|
.expect("set approval policy");
|
|
config
|
|
.permissions
|
|
.set_permission_profile_from_session_snapshot(PermissionProfileSnapshot::active(
|
|
PermissionProfile::workspace_write(),
|
|
ActivePermissionProfile::new(BUILT_IN_PERMISSION_PROFILE_WORKSPACE),
|
|
))
|
|
.expect("set permission profile");
|
|
|
|
assert_eq!(
|
|
permissions_text_for(&config).as_deref(),
|
|
Some("Workspace (Approve for me)")
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn status_permissions_named_profile_shows_additional_writable_roots() {
|
|
let temp_home = TempDir::new().expect("temp home");
|
|
let mut config = test_config(&temp_home).await;
|
|
config
|
|
.permissions
|
|
.approval_policy
|
|
.set(AskForApproval::OnRequest.to_core())
|
|
.expect("set approval policy");
|
|
let extra_root = test_path_buf("/workspace/extra").abs();
|
|
config
|
|
.permissions
|
|
.set_permission_profile_from_session_snapshot(PermissionProfileSnapshot::active(
|
|
PermissionProfile::workspace_write_with(
|
|
std::slice::from_ref(&extra_root),
|
|
NetworkSandboxPolicy::Restricted,
|
|
/*exclude_tmpdir_env_var*/ false,
|
|
/*exclude_slash_tmp*/ false,
|
|
),
|
|
ActivePermissionProfile::new(BUILT_IN_PERMISSION_PROFILE_WORKSPACE),
|
|
))
|
|
.expect("set permission profile");
|
|
|
|
assert_eq!(
|
|
permissions_text_for(&config).as_deref(),
|
|
Some("Workspace (Ask for approval)")
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn status_permissions_workspace_roots_show_additional_directories() {
|
|
let temp_home = TempDir::new().expect("temp home");
|
|
let mut config = test_config(&temp_home).await;
|
|
set_workspace_cwd(&mut config, test_path_buf("/workspace/tests").abs());
|
|
config
|
|
.permissions
|
|
.approval_policy
|
|
.set(AskForApproval::OnRequest.to_core())
|
|
.expect("set approval policy");
|
|
let extra_root = test_path_buf("/workspace/extra").abs();
|
|
config.workspace_roots = vec![config.cwd.clone(), extra_root.clone()];
|
|
config
|
|
.permissions
|
|
.set_workspace_roots(config.workspace_roots.clone());
|
|
config
|
|
.permissions
|
|
.set_permission_profile_from_session_snapshot(PermissionProfileSnapshot::active(
|
|
PermissionProfile::workspace_write(),
|
|
ActivePermissionProfile::new(":workspace"),
|
|
))
|
|
.expect("set permission profile");
|
|
|
|
assert_eq!(
|
|
permissions_text_for(&config),
|
|
Some(format!(
|
|
"Workspace [{}] (Ask for approval)",
|
|
extra_root.display()
|
|
))
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn status_permissions_workspace_roots_include_profile_defined_directories() {
|
|
let temp_home = TempDir::new().expect("temp home");
|
|
let mut config = test_config(&temp_home).await;
|
|
set_workspace_cwd(&mut config, test_path_buf("/workspace/tests").abs());
|
|
config
|
|
.permissions
|
|
.approval_policy
|
|
.set(AskForApproval::OnRequest.to_core())
|
|
.expect("set approval policy");
|
|
let profile_root = test_path_buf("/workspace/shared").abs();
|
|
config
|
|
.permissions
|
|
.set_permission_profile_from_session_snapshot(
|
|
PermissionProfileSnapshot::active_with_profile_workspace_roots(
|
|
PermissionProfile::workspace_write_with(
|
|
std::slice::from_ref(&profile_root),
|
|
NetworkSandboxPolicy::Restricted,
|
|
/*exclude_tmpdir_env_var*/ false,
|
|
/*exclude_slash_tmp*/ false,
|
|
),
|
|
ActivePermissionProfile::new(":workspace"),
|
|
vec![profile_root.clone()],
|
|
),
|
|
)
|
|
.expect("set permission profile");
|
|
|
|
assert_eq!(
|
|
permissions_text_for(&config),
|
|
Some(format!(
|
|
"Workspace [{}] (Ask for approval)",
|
|
profile_root.display()
|
|
))
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn status_permissions_broadened_workspace_profile_shows_builtin_label() {
|
|
let temp_home = TempDir::new().expect("temp home");
|
|
let mut config = test_config(&temp_home).await;
|
|
config
|
|
.permissions
|
|
.approval_policy
|
|
.set(AskForApproval::OnRequest.to_core())
|
|
.expect("set approval policy");
|
|
config
|
|
.permissions
|
|
.set_permission_profile_from_session_snapshot(PermissionProfileSnapshot::active(
|
|
PermissionProfile::workspace_write_with(
|
|
&[],
|
|
NetworkSandboxPolicy::Enabled,
|
|
/*exclude_tmpdir_env_var*/ false,
|
|
/*exclude_slash_tmp*/ false,
|
|
),
|
|
ActivePermissionProfile::new(BUILT_IN_PERMISSION_PROFILE_WORKSPACE),
|
|
))
|
|
.expect("set permission profile");
|
|
|
|
assert_eq!(
|
|
permissions_text_for(&config).as_deref(),
|
|
Some("Workspace with network access (Ask for approval)")
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn status_permissions_user_defined_profile_shows_name() {
|
|
let temp_home = TempDir::new().expect("temp home");
|
|
let mut config = test_config(&temp_home).await;
|
|
config
|
|
.permissions
|
|
.set_permission_profile_from_session_snapshot(PermissionProfileSnapshot::active(
|
|
PermissionProfile::read_only(),
|
|
ActivePermissionProfile::new("locked"),
|
|
))
|
|
.expect("set permission profile");
|
|
|
|
assert_eq!(
|
|
permissions_text_for(&config).as_deref(),
|
|
Some("Profile locked (read-only, Ask for approval)")
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn status_snapshot_shows_active_user_defined_profile() {
|
|
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());
|
|
set_workspace_cwd(&mut config, test_path_buf("/workspace/tests").abs());
|
|
config
|
|
.permissions
|
|
.set_permission_profile_from_session_snapshot(PermissionProfileSnapshot::active(
|
|
PermissionProfile::read_only(),
|
|
ActivePermissionProfile::new("locked"),
|
|
))
|
|
.expect("set permission profile");
|
|
|
|
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 token_info = token_info_for(&model_slug, &config, &usage);
|
|
|
|
let composite = new_status_output(
|
|
&config,
|
|
test_status_account_display().as_ref(),
|
|
Some(&token_info),
|
|
&usage,
|
|
&None,
|
|
/*thread_name*/ None,
|
|
/*forked_from*/ None,
|
|
/*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_model_provider_uses_bedrock_runtime_base_url_and_gates_usage_link() {
|
|
let temp_home = TempDir::new().expect("temp home");
|
|
let mut config = test_config(&temp_home).await;
|
|
config.model_provider_id = "amazon-bedrock".to_string();
|
|
config.model_provider =
|
|
ModelProviderInfo::create_amazon_bedrock_provider(Some(ModelProviderAwsAuthInfo {
|
|
profile: None,
|
|
region: Some("eu-west-1".to_string()),
|
|
}));
|
|
config.model_provider.base_url =
|
|
Some("https://bedrock-mantle.us-east-1.api.aws/openai/v1".to_string());
|
|
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 runtime_base_url = "https://bedrock-mantle.eu-west-1.api.aws/openai/v1";
|
|
|
|
let (composite, _handle) = new_status_output_with_rate_limits_handle(
|
|
&config,
|
|
Some(runtime_base_url),
|
|
/*remote_connection*/ None,
|
|
test_status_account_display().as_ref(),
|
|
/*token_info*/ None,
|
|
&usage,
|
|
&None,
|
|
/*thread_name*/ None,
|
|
/*forked_from*/ None,
|
|
/*rate_limits*/ &[],
|
|
None,
|
|
captured_at,
|
|
&model_slug,
|
|
/*collaboration_mode*/ None,
|
|
/*reasoning_effort_override*/ None,
|
|
"<none>".to_string(),
|
|
/*refreshing_rate_limits*/ false,
|
|
);
|
|
let rendered = render_lines(&composite.display_lines(/*width*/ 120)).join("\n");
|
|
|
|
assert!(
|
|
rendered.contains(&format!("Amazon Bedrock - {runtime_base_url}")),
|
|
"expected /status to render runtime Bedrock URL, got: {rendered}"
|
|
);
|
|
assert!(
|
|
!rendered.contains("bedrock-mantle.us-east-1"),
|
|
"expected /status to ignore configured Bedrock base URL, got: {rendered}"
|
|
);
|
|
assert!(
|
|
!rendered.contains("https://chatgpt.com/codex/settings/usage"),
|
|
"expected /status to hide ChatGPT usage link for Bedrock, got: {rendered}"
|
|
);
|
|
|
|
config.model_provider_id = "openai-proxy".to_string();
|
|
config.model_provider = ModelProviderInfo {
|
|
name: "OpenAI Proxy".to_string(),
|
|
base_url: Some("https://openai-proxy.example/v1".to_string()),
|
|
requires_openai_auth: true,
|
|
..ModelProviderInfo::default()
|
|
};
|
|
let (composite, _handle) = new_status_output_with_rate_limits_handle(
|
|
&config,
|
|
/*runtime_model_provider_base_url*/ None,
|
|
/*remote_connection*/ None,
|
|
test_status_account_display().as_ref(),
|
|
/*token_info*/ None,
|
|
&usage,
|
|
&None,
|
|
/*thread_name*/ None,
|
|
/*forked_from*/ None,
|
|
/*rate_limits*/ &[],
|
|
None,
|
|
captured_at,
|
|
&model_slug,
|
|
/*collaboration_mode*/ None,
|
|
/*reasoning_effort_override*/ None,
|
|
"<none>".to_string(),
|
|
/*refreshing_rate_limits*/ false,
|
|
);
|
|
let rendered = render_lines(&composite.display_lines(/*width*/ 120)).join("\n");
|
|
|
|
assert!(
|
|
rendered.contains("https://chatgpt.com/codex/settings/usage"),
|
|
"expected /status to show ChatGPT usage link for OpenAI-auth proxy, got: {rendered}"
|
|
);
|
|
|
|
let wide_destinations: Vec<String> = composite
|
|
.display_hyperlink_lines(/*width*/ 120)
|
|
.into_iter()
|
|
.flat_map(|line| line.hyperlinks.into_iter())
|
|
.map(|link| link.destination)
|
|
.collect();
|
|
assert_eq!(
|
|
wide_destinations,
|
|
vec!["https://chatgpt.com/codex/settings/usage"]
|
|
);
|
|
|
|
let narrow_destinations: Vec<String> = composite
|
|
.display_hyperlink_lines(/*width*/ 24)
|
|
.into_iter()
|
|
.flat_map(|line| line.hyperlinks.into_iter())
|
|
.map(|link| link.destination)
|
|
.collect();
|
|
assert_eq!(narrow_destinations, Vec::<String>::new());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn status_snapshot_shows_auto_review_permissions() {
|
|
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());
|
|
set_workspace_cwd(&mut config, test_path_buf("/workspace/tests").abs());
|
|
config.approvals_reviewer = ApprovalsReviewer::AutoReview;
|
|
config
|
|
.permissions
|
|
.set_permission_profile_from_session_snapshot(PermissionProfileSnapshot::active(
|
|
PermissionProfile::workspace_write(),
|
|
ActivePermissionProfile::new(BUILT_IN_PERMISSION_PROFILE_WORKSPACE),
|
|
))
|
|
.expect("set permission profile");
|
|
|
|
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 token_info = token_info_for(&model_slug, &config, &usage);
|
|
|
|
let composite = new_status_output(
|
|
&config,
|
|
test_status_account_display().as_ref(),
|
|
Some(&token_info),
|
|
&usage,
|
|
&None,
|
|
/*thread_name*/ None,
|
|
/*forked_from*/ None,
|
|
/*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_permissions_full_disk_managed_with_network_is_danger_full_access() {
|
|
let temp_home = TempDir::new().expect("temp home");
|
|
let mut config = test_config(&temp_home).await;
|
|
config
|
|
.permissions
|
|
.approval_policy
|
|
.set(AskForApproval::OnRequest.to_core())
|
|
.expect("set approval policy");
|
|
config
|
|
.permissions
|
|
.set_permission_profile(PermissionProfile::Managed {
|
|
network: NetworkSandboxPolicy::Enabled,
|
|
file_system: ManagedFileSystemPermissions::Unrestricted,
|
|
})
|
|
.expect("set permission profile");
|
|
|
|
assert_eq!(
|
|
permissions_text_for(&config).as_deref(),
|
|
Some("Custom (danger-full-access, Ask for approval)")
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn status_permissions_full_disk_managed_without_network_is_external_sandbox() {
|
|
let temp_home = TempDir::new().expect("temp home");
|
|
let mut config = test_config(&temp_home).await;
|
|
config
|
|
.permissions
|
|
.approval_policy
|
|
.set(AskForApproval::OnRequest.to_core())
|
|
.expect("set approval policy");
|
|
config
|
|
.permissions
|
|
.set_permission_profile(PermissionProfile::Managed {
|
|
network: NetworkSandboxPolicy::Restricted,
|
|
file_system: ManagedFileSystemPermissions::Unrestricted,
|
|
})
|
|
.expect("set permission profile");
|
|
|
|
assert_eq!(
|
|
permissions_text_for(&config).as_deref(),
|
|
Some("Custom (external-sandbox, Ask for approval)")
|
|
);
|
|
}
|
|
|
|
#[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();
|
|
set_workspace_cwd(&mut config, test_path_buf("/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();
|
|
set_workspace_cwd(&mut config, test_path_buf("/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,
|
|
window_duration_mins: Some(43_200),
|
|
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 86_400)),
|
|
}),
|
|
secondary: None,
|
|
credits: None,
|
|
individual_limit: None,
|
|
plan_type: None,
|
|
rate_limit_reached_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_includes_enterprise_monthly_credit_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();
|
|
set_workspace_cwd(&mut config, test_path_buf("/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: None,
|
|
secondary: None,
|
|
credits: None,
|
|
individual_limit: Some(SpendControlLimitSnapshot {
|
|
limit: "25000".to_string(),
|
|
used: "8000".to_string(),
|
|
remaining_percent: 68,
|
|
resets_at: reset_at_from(&captured_at, /*seconds*/ 86_400),
|
|
}),
|
|
plan_type: None,
|
|
rate_limit_reached_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*/ 92));
|
|
if cfg!(windows) {
|
|
for line in &mut rendered_lines {
|
|
*line = line.replace('\\', "/");
|
|
}
|
|
}
|
|
let sanitized = sanitize_directory(rendered_lines).join("\n");
|
|
assert_snapshot!(sanitized);
|
|
|
|
let mut rendered_lines = render_lines(&composite.display_lines(/*width*/ 46));
|
|
if cfg!(windows) {
|
|
for line in &mut rendered_lines {
|
|
*line = line.replace('\\', "/");
|
|
}
|
|
}
|
|
let sanitized = sanitize_directory(rendered_lines).join("\n");
|
|
assert_snapshot!(
|
|
"status_snapshot_wraps_enterprise_monthly_credit_details_in_narrow_terminal",
|
|
sanitized
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn status_snapshot_uses_generic_limit_labels_for_unsupported_windows() {
|
|
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();
|
|
set_workspace_cwd(&mut config, test_path_buf("/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: 35,
|
|
window_duration_mins: Some(2 * 60),
|
|
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 86_400)),
|
|
}),
|
|
secondary: Some(RateLimitWindow {
|
|
used_percent: 50,
|
|
window_duration_mins: Some(3 * 60),
|
|
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 172_800)),
|
|
}),
|
|
credits: None,
|
|
individual_limit: None,
|
|
plan_type: None,
|
|
rate_limit_reached_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,
|
|
}),
|
|
individual_limit: None,
|
|
plan_type: None,
|
|
rate_limit_reached_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()),
|
|
}),
|
|
individual_limit: None,
|
|
plan_type: None,
|
|
rate_limit_reached_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()),
|
|
}),
|
|
individual_limit: None,
|
|
plan_type: None,
|
|
rate_limit_reached_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,
|
|
}),
|
|
individual_limit: None,
|
|
plan_type: None,
|
|
rate_limit_reached_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());
|
|
set_workspace_cwd(&mut config, test_path_buf("/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);
|
|
set_workspace_cwd(&mut config, test_path_buf("/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,
|
|
window_duration_mins: Some(300),
|
|
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 600)),
|
|
}),
|
|
secondary: None,
|
|
credits: None,
|
|
individual_limit: None,
|
|
plan_type: None,
|
|
rate_limit_reached_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());
|
|
set_workspace_cwd(&mut config, test_path_buf("/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_uses_default_reasoning_when_config_empty() {
|
|
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());
|
|
set_workspace_cwd(&mut config, test_path_buf("/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 remote_connection = RemoteConnectionStatus {
|
|
address: "unix:///tmp/codex-home/app-server-control/app-server-control.sock".to_string(),
|
|
version: "v0.133.0".to_string(),
|
|
};
|
|
|
|
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_handle(
|
|
&config,
|
|
/*runtime_model_provider_base_url*/ None,
|
|
Some(&remote_connection),
|
|
account_display.as_ref(),
|
|
Some(&token_info),
|
|
&usage,
|
|
&None,
|
|
/*thread_name*/ None,
|
|
/*forked_from*/ None,
|
|
&[],
|
|
None,
|
|
now,
|
|
&model_slug,
|
|
/*collaboration_mode*/ None,
|
|
/*reasoning_effort_override*/ Some(Some(ReasoningEffort::Medium)),
|
|
"<none>".to_string(),
|
|
/*refreshing_rate_limits*/ false,
|
|
);
|
|
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());
|
|
set_workspace_cwd(&mut config, test_path_buf("/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,
|
|
window_duration_mins: Some(300),
|
|
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 900)),
|
|
}),
|
|
secondary: Some(RateLimitWindow {
|
|
used_percent: 30,
|
|
window_duration_mins: Some(10_080),
|
|
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 2_700)),
|
|
}),
|
|
credits: None,
|
|
individual_limit: None,
|
|
plan_type: None,
|
|
rate_limit_reached_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());
|
|
set_workspace_cwd(&mut config, test_path_buf("/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,
|
|
window_duration_mins: Some(300),
|
|
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 900)),
|
|
}),
|
|
secondary: Some(RateLimitWindow {
|
|
used_percent: 30,
|
|
window_duration_mins: 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()),
|
|
}),
|
|
individual_limit: None,
|
|
plan_type: None,
|
|
rate_limit_reached_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());
|
|
set_workspace_cwd(&mut config, test_path_buf("/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,
|
|
individual_limit: None,
|
|
plan_type: None,
|
|
rate_limit_reached_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());
|
|
set_workspace_cwd(&mut config, test_path_buf("/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,
|
|
individual_limit: None,
|
|
plan_type: None,
|
|
rate_limit_reached_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());
|
|
set_workspace_cwd(&mut config, test_path_buf("/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,
|
|
window_duration_mins: Some(300),
|
|
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 600)),
|
|
}),
|
|
secondary: Some(RateLimitWindow {
|
|
used_percent: 40,
|
|
window_duration_mins: Some(10_080),
|
|
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 1_800)),
|
|
}),
|
|
credits: None,
|
|
individual_limit: None,
|
|
plan_type: None,
|
|
rate_limit_reached_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());
|
|
set_workspace_cwd(&mut config, test_path_buf("/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,
|
|
window_duration_mins: Some(300),
|
|
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 1_200)),
|
|
}),
|
|
secondary: Some(RateLimitWindow {
|
|
used_percent: 35,
|
|
window_duration_mins: 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()),
|
|
}),
|
|
individual_limit: None,
|
|
plan_type: None,
|
|
rate_limit_reached_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}"
|
|
);
|
|
}
|