Files
codex/codex-rs/tui/src/status/tests.rs
T
Michael Bolin 108234b5eb core: set permission profiles from snapshots (#22920)
## Why

#22891 moved the TUI turn-command path to pass `ActivePermissionProfile`
instead of the full `PermissionProfile`, but the remaining
config/session bridge still accepted the concrete `PermissionProfile`
and active profile id as separate arguments. That shape made it too easy
for future callers to update the concrete profile and active profile id
out of sync.

This PR makes the trusted session snapshot path pass one coherent value
into `Permissions`, while keeping `requirements.toml` enforcement owned
by the existing constrained permission state.

## What Changed

- Added `PermissionProfileSnapshot` as the public snapshot value for
trusted session/config synchronization.
- Changed `Permissions::set_permission_profile_from_session_snapshot()`
and `replace_permission_profile_from_session_snapshot()` to take a
`PermissionProfileSnapshot`.
- Updated the replacement path to derive its constrained
`PermissionProfile` from the snapshot, so callers cannot pass a separate
profile that disagrees with the snapshot.
- Removed the internal tuple-style
`PermissionProfileState::set_active_permission_profile()` mutation path.
- Updated core session projection and TUI call sites to construct
explicit legacy or active snapshots.
- Documented the snapshot constructors so legacy use and id/profile
mismatch hazards are called out at the API boundary.
- Added a focused config test that verifies snapshot updates still
respect existing permission constraints.

## How To Review

1. Start with `codex-rs/core/src/config/resolved_permission_profile.rs`;
`PermissionProfileSnapshot` is the public wrapper, while
`ResolvedPermissionProfile` stays internal.
2. Check `codex-rs/core/src/config/mod.rs` to confirm both
session-snapshot setters validate through `PermissionProfileState` and
no longer accept loose profile/id pairs.
3. Skim `codex-rs/core/src/session/session.rs` for the session
projection path; it now builds the snapshot before installing it.
4. Skim the TUI changes as call-site migration from loose argument pairs
to explicit snapshot construction.

## Verification

- `cargo test -p codex-core
permission_snapshot_setter_preserves_permission_constraints`
- `cargo test -p codex-tui status_permissions_`
- `cargo test -p codex-tui
session_configured_preserves_profile_workspace_roots`
- `just fix -p codex-core -p codex-tui`
2026-05-16 07:26:18 -07:00

1704 lines
57 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 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::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::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_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;
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> {
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()
}
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,
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, on-request)")
);
}
#[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 (on-request)")
);
}
#[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 (on-request)")
);
}
#[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 (on-request)")
);
}
#[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 (auto-review)")
);
}
#[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 (on-request)")
);
}
#[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 [{}] (on-request)", 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 [{}] (on-request)",
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 (on-request)")
);
}
#[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, on-request)")
);
}
#[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() {
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),
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}"
);
}
#[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, on-request)")
);
}
#[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, 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();
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,
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,
}),
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()),
}),
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()),
}),
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,
}),
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,
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 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,
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,
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()),
}),
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,
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,
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,
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()),
}),
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}"
);
}