mirror of
https://github.com/openai/codex.git
synced 2026-04-30 01:16:54 +00:00
[codex] Add owner nudge app-server API (#18220)
## Summary Second PR in the split from #17956. Stacked on #18227. - adds app-server v2 protocol/schema support for `account/sendAddCreditsNudgeEmail` - adds the backend-client `send_add_credits_nudge_email` request and request body mapping - handles the app-server request with auth checks, backend call, and cooldown mapping - adds the disabled `workspace_owner_usage_nudge` feature flag and focused app-server/backend tests ## Validation - `cargo test -p codex-backend-client` - `cargo test -p codex-app-server-protocol` - `cargo test -p codex-app-server rate_limits` - `cargo test -p codex-tui workspace_` - `cargo test -p codex-tui status_` - `just fmt` - `just fix -p codex-backend-client` - `just fix -p codex-app-server-protocol` - `just fix -p codex-app-server` - `just fix -p codex-tui`
This commit is contained in:
@@ -3,6 +3,8 @@ use app_test_support::ChatGptAuthFixture;
|
||||
use app_test_support::McpProcess;
|
||||
use app_test_support::to_response;
|
||||
use app_test_support::write_chatgpt_auth;
|
||||
use codex_app_server_protocol::AddCreditsNudgeCreditType;
|
||||
use codex_app_server_protocol::AddCreditsNudgeEmailStatus;
|
||||
use codex_app_server_protocol::GetAccountRateLimitsResponse;
|
||||
use codex_app_server_protocol::JSONRPCError;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
@@ -11,6 +13,8 @@ use codex_app_server_protocol::RateLimitReachedType;
|
||||
use codex_app_server_protocol::RateLimitSnapshot;
|
||||
use codex_app_server_protocol::RateLimitWindow;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::SendAddCreditsNudgeEmailParams;
|
||||
use codex_app_server_protocol::SendAddCreditsNudgeEmailResponse;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_protocol::account::PlanType as AccountPlanType;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -27,6 +31,7 @@ use wiremock::matchers::path;
|
||||
|
||||
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
const INVALID_REQUEST_ERROR_CODE: i64 = -32600;
|
||||
const INTERNAL_ERROR_CODE: i64 = -32603;
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_account_rate_limits_requires_auth() -> Result<()> {
|
||||
@@ -229,6 +234,209 @@ async fn get_account_rate_limits_returns_snapshot() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn send_add_credits_nudge_email_requires_auth() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_add_credits_nudge_email_request(SendAddCreditsNudgeEmailParams {
|
||||
credit_type: AddCreditsNudgeCreditType::Credits,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let error: JSONRPCError = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
|
||||
assert_eq!(error.id, RequestId::Integer(request_id));
|
||||
assert_eq!(error.error.code, INVALID_REQUEST_ERROR_CODE);
|
||||
assert_eq!(
|
||||
error.error.message,
|
||||
"codex account authentication required to notify workspace owner"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn send_add_credits_nudge_email_requires_chatgpt_auth() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
login_with_api_key(&mut mcp, "sk-test-key").await?;
|
||||
|
||||
let request_id = mcp
|
||||
.send_add_credits_nudge_email_request(SendAddCreditsNudgeEmailParams {
|
||||
credit_type: AddCreditsNudgeCreditType::UsageLimit,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let error: JSONRPCError = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
|
||||
assert_eq!(error.id, RequestId::Integer(request_id));
|
||||
assert_eq!(error.error.code, INVALID_REQUEST_ERROR_CODE);
|
||||
assert_eq!(
|
||||
error.error.message,
|
||||
"chatgpt authentication required to notify workspace owner"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg_attr(target_os = "windows", ignore = "covered by Linux and macOS CI")]
|
||||
#[tokio::test]
|
||||
async fn send_add_credits_nudge_email_posts_expected_body() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
write_chatgpt_auth(
|
||||
codex_home.path(),
|
||||
ChatGptAuthFixture::new("chatgpt-token")
|
||||
.account_id("account-123")
|
||||
.plan_type("pro"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let server = MockServer::start().await;
|
||||
let server_url = server.uri();
|
||||
write_chatgpt_base_url(codex_home.path(), &server_url)?;
|
||||
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/codex/accounts/send_add_credits_nudge_email"))
|
||||
.and(header("authorization", "Bearer chatgpt-token"))
|
||||
.and(header("chatgpt-account-id", "account-123"))
|
||||
.and(wiremock::matchers::body_json(json!({
|
||||
"credit_type": "usage_limit",
|
||||
})))
|
||||
.respond_with(ResponseTemplate::new(200))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_add_credits_nudge_email_request(SendAddCreditsNudgeEmailParams {
|
||||
credit_type: AddCreditsNudgeCreditType::UsageLimit,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let received: SendAddCreditsNudgeEmailResponse = to_response(response)?;
|
||||
|
||||
assert_eq!(received.status, AddCreditsNudgeEmailStatus::Sent);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg_attr(target_os = "windows", ignore = "covered by Linux and macOS CI")]
|
||||
#[tokio::test]
|
||||
async fn send_add_credits_nudge_email_maps_cooldown() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
write_chatgpt_auth(
|
||||
codex_home.path(),
|
||||
ChatGptAuthFixture::new("chatgpt-token")
|
||||
.account_id("account-123")
|
||||
.plan_type("pro"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let server = MockServer::start().await;
|
||||
let server_url = server.uri();
|
||||
write_chatgpt_base_url(codex_home.path(), &server_url)?;
|
||||
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/codex/accounts/send_add_credits_nudge_email"))
|
||||
.respond_with(ResponseTemplate::new(429))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_add_credits_nudge_email_request(SendAddCreditsNudgeEmailParams {
|
||||
credit_type: AddCreditsNudgeCreditType::Credits,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let received: SendAddCreditsNudgeEmailResponse = to_response(response)?;
|
||||
|
||||
assert_eq!(received.status, AddCreditsNudgeEmailStatus::CooldownActive);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg_attr(target_os = "windows", ignore = "covered by Linux and macOS CI")]
|
||||
#[tokio::test]
|
||||
async fn send_add_credits_nudge_email_surfaces_backend_failure() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
write_chatgpt_auth(
|
||||
codex_home.path(),
|
||||
ChatGptAuthFixture::new("chatgpt-token")
|
||||
.account_id("account-123")
|
||||
.plan_type("pro"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let server = MockServer::start().await;
|
||||
let server_url = server.uri();
|
||||
write_chatgpt_base_url(codex_home.path(), &server_url)?;
|
||||
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/codex/accounts/send_add_credits_nudge_email"))
|
||||
.respond_with(ResponseTemplate::new(500).set_body_string("boom"))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_add_credits_nudge_email_request(SendAddCreditsNudgeEmailParams {
|
||||
credit_type: AddCreditsNudgeCreditType::Credits,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let error: JSONRPCError = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
|
||||
assert_eq!(error.id, RequestId::Integer(request_id));
|
||||
assert_eq!(error.error.code, INTERNAL_ERROR_CODE);
|
||||
assert!(
|
||||
error
|
||||
.error
|
||||
.message
|
||||
.contains("failed to notify workspace owner"),
|
||||
"unexpected error message: {}",
|
||||
error.error.message
|
||||
);
|
||||
assert_eq!(error.error.data, None);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn login_with_api_key(mcp: &mut McpProcess, api_key: &str) -> Result<()> {
|
||||
let request_id = mcp.send_login_account_api_key_request(api_key).await?;
|
||||
let response: JSONRPCResponse = timeout(
|
||||
|
||||
Reference in New Issue
Block a user