mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
## Why `argument-comment-lint` was green in CI even though the repo still had many uncommented literal arguments. The main gap was target coverage: the repo wrapper did not force Cargo to inspect test-only call sites, so examples like the `latest_session_lookup_params(true, ...)` tests in `codex-rs/tui_app_server/src/lib.rs` never entered the blocking CI path. This change cleans up the existing backlog, makes the default repo lint path cover all Cargo targets, and starts rolling that stricter CI enforcement out on the platform where it is currently validated. ## What changed - mechanically fixed existing `argument-comment-lint` violations across the `codex-rs` workspace, including tests, examples, and benches - updated `tools/argument-comment-lint/run-prebuilt-linter.sh` and `tools/argument-comment-lint/run.sh` so non-`--fix` runs default to `--all-targets` unless the caller explicitly narrows the target set - fixed both wrappers so forwarded cargo arguments after `--` are preserved with a single separator - documented the new default behavior in `tools/argument-comment-lint/README.md` - updated `rust-ci` so the macOS lint lane keeps the plain wrapper invocation and therefore enforces `--all-targets`, while Linux and Windows temporarily pass `-- --lib --bins` That temporary CI split keeps the stricter all-targets check where it is already cleaned up, while leaving room to finish the remaining Linux- and Windows-specific target-gated cleanup before enabling `--all-targets` on those runners. The Linux and Windows failures on the intermediate revision were caused by the wrapper forwarding bug, not by additional lint findings in those lanes. ## Validation - `bash -n tools/argument-comment-lint/run.sh` - `bash -n tools/argument-comment-lint/run-prebuilt-linter.sh` - shell-level wrapper forwarding check for `-- --lib --bins` - shell-level wrapper forwarding check for `-- --tests` - `just argument-comment-lint` - `cargo test` in `tools/argument-comment-lint` - `cargo test -p codex-terminal-detection` ## Follow-up - Clean up remaining Linux-only target-gated callsites, then switch the Linux lint lane back to the plain wrapper invocation. - Clean up remaining Windows-only target-gated callsites, then switch the Windows lint lane back to the plain wrapper invocation.
334 lines
10 KiB
Rust
334 lines
10 KiB
Rust
#![allow(clippy::unwrap_used)]
|
|
|
|
use anyhow::Context;
|
|
use base64::Engine;
|
|
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
|
use codex_login::ServerOptions;
|
|
use codex_login::auth::AuthCredentialsStoreMode;
|
|
use codex_login::auth::load_auth_dot_json;
|
|
use codex_login::run_device_code_login;
|
|
use serde_json::json;
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::AtomicUsize;
|
|
use std::sync::atomic::Ordering;
|
|
use tempfile::tempdir;
|
|
use wiremock::Mock;
|
|
use wiremock::MockServer;
|
|
use wiremock::Request;
|
|
use wiremock::ResponseTemplate;
|
|
use wiremock::matchers::method;
|
|
use wiremock::matchers::path;
|
|
|
|
use core_test_support::skip_if_no_network;
|
|
|
|
// ---------- Small helpers ----------
|
|
|
|
fn make_jwt(payload: serde_json::Value) -> String {
|
|
let header = json!({ "alg": "none", "typ": "JWT" });
|
|
let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&header).unwrap());
|
|
let payload_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&payload).unwrap());
|
|
let signature_b64 = URL_SAFE_NO_PAD.encode(b"sig");
|
|
format!("{header_b64}.{payload_b64}.{signature_b64}")
|
|
}
|
|
|
|
async fn mock_usercode_success(server: &MockServer) {
|
|
Mock::given(method("POST"))
|
|
.and(path("/api/accounts/deviceauth/usercode"))
|
|
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
|
"device_auth_id": "device-auth-123",
|
|
"user_code": "CODE-12345",
|
|
// NOTE: Interval is kept 0 in order to avoid waiting for the interval to pass
|
|
"interval": "0"
|
|
})))
|
|
.mount(server)
|
|
.await;
|
|
}
|
|
|
|
async fn mock_usercode_failure(server: &MockServer, status: u16) {
|
|
Mock::given(method("POST"))
|
|
.and(path("/api/accounts/deviceauth/usercode"))
|
|
.respond_with(ResponseTemplate::new(status))
|
|
.mount(server)
|
|
.await;
|
|
}
|
|
|
|
async fn mock_poll_token_two_step(
|
|
server: &MockServer,
|
|
counter: Arc<AtomicUsize>,
|
|
first_response_status: u16,
|
|
) {
|
|
let c = counter.clone();
|
|
Mock::given(method("POST"))
|
|
.and(path("/api/accounts/deviceauth/token"))
|
|
.respond_with(move |_: &Request| {
|
|
let attempt = c.fetch_add(1, Ordering::SeqCst);
|
|
if attempt == 0 {
|
|
ResponseTemplate::new(first_response_status)
|
|
} else {
|
|
ResponseTemplate::new(200).set_body_json(json!({
|
|
"authorization_code": "poll-code-321",
|
|
"code_challenge": "code-challenge-321",
|
|
"code_verifier": "code-verifier-321"
|
|
}))
|
|
}
|
|
})
|
|
.expect(2)
|
|
.mount(server)
|
|
.await;
|
|
}
|
|
|
|
async fn mock_poll_token_single(server: &MockServer, endpoint: &str, response: ResponseTemplate) {
|
|
Mock::given(method("POST"))
|
|
.and(path(endpoint))
|
|
.respond_with(response)
|
|
.mount(server)
|
|
.await;
|
|
}
|
|
|
|
async fn mock_oauth_token_single(server: &MockServer, jwt: String) {
|
|
Mock::given(method("POST"))
|
|
.and(path("/oauth/token"))
|
|
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
|
"id_token": jwt.clone(),
|
|
"access_token": "access-token-123",
|
|
"refresh_token": "refresh-token-123"
|
|
})))
|
|
.mount(server)
|
|
.await;
|
|
}
|
|
|
|
fn server_opts(
|
|
codex_home: &tempfile::TempDir,
|
|
issuer: String,
|
|
cli_auth_credentials_store_mode: AuthCredentialsStoreMode,
|
|
) -> ServerOptions {
|
|
let mut opts = ServerOptions::new(
|
|
codex_home.path().to_path_buf(),
|
|
"client-id".to_string(),
|
|
/*forced_chatgpt_workspace_id*/ None,
|
|
cli_auth_credentials_store_mode,
|
|
);
|
|
opts.issuer = issuer;
|
|
opts.open_browser = false;
|
|
opts
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn device_code_login_integration_succeeds() -> anyhow::Result<()> {
|
|
skip_if_no_network!(Ok(()));
|
|
|
|
let codex_home = tempdir().unwrap();
|
|
let mock_server = MockServer::start().await;
|
|
|
|
mock_usercode_success(&mock_server).await;
|
|
|
|
mock_poll_token_two_step(
|
|
&mock_server,
|
|
Arc::new(AtomicUsize::new(0)),
|
|
/*first_response_status*/ 404,
|
|
)
|
|
.await;
|
|
|
|
let jwt = make_jwt(json!({
|
|
"https://api.openai.com/auth": {
|
|
"chatgpt_account_id": "acct_321"
|
|
}
|
|
}));
|
|
|
|
mock_oauth_token_single(&mock_server, jwt.clone()).await;
|
|
|
|
let issuer = mock_server.uri();
|
|
let opts = server_opts(&codex_home, issuer, AuthCredentialsStoreMode::File);
|
|
|
|
run_device_code_login(opts)
|
|
.await
|
|
.expect("device code login integration should succeed");
|
|
|
|
let auth = load_auth_dot_json(codex_home.path(), AuthCredentialsStoreMode::File)
|
|
.context("auth.json should load after login succeeds")?
|
|
.context("auth.json written")?;
|
|
// assert_eq!(auth.openai_api_key.as_deref(), Some("api-key-321"));
|
|
let tokens = auth.tokens.expect("tokens persisted");
|
|
assert_eq!(tokens.access_token, "access-token-123");
|
|
assert_eq!(tokens.refresh_token, "refresh-token-123");
|
|
assert_eq!(tokens.id_token.raw_jwt, jwt);
|
|
assert_eq!(tokens.account_id.as_deref(), Some("acct_321"));
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn device_code_login_rejects_workspace_mismatch() -> anyhow::Result<()> {
|
|
skip_if_no_network!(Ok(()));
|
|
|
|
let codex_home = tempdir().unwrap();
|
|
let mock_server = MockServer::start().await;
|
|
|
|
mock_usercode_success(&mock_server).await;
|
|
|
|
mock_poll_token_two_step(
|
|
&mock_server,
|
|
Arc::new(AtomicUsize::new(0)),
|
|
/*first_response_status*/ 404,
|
|
)
|
|
.await;
|
|
|
|
let jwt = make_jwt(json!({
|
|
"https://api.openai.com/auth": {
|
|
"chatgpt_account_id": "acct_321",
|
|
"organization_id": "org-actual"
|
|
}
|
|
}));
|
|
|
|
mock_oauth_token_single(&mock_server, jwt).await;
|
|
|
|
let issuer = mock_server.uri();
|
|
let mut opts = server_opts(&codex_home, issuer, AuthCredentialsStoreMode::File);
|
|
opts.forced_chatgpt_workspace_id = Some("org-required".to_string());
|
|
|
|
let err = run_device_code_login(opts)
|
|
.await
|
|
.expect_err("device code login should fail when workspace mismatches");
|
|
assert_eq!(err.kind(), std::io::ErrorKind::PermissionDenied);
|
|
|
|
let auth = load_auth_dot_json(codex_home.path(), AuthCredentialsStoreMode::File)
|
|
.context("auth.json should load after login fails")?;
|
|
assert!(
|
|
auth.is_none(),
|
|
"auth.json should not be created when workspace validation fails"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn device_code_login_integration_handles_usercode_http_failure() -> anyhow::Result<()> {
|
|
skip_if_no_network!(Ok(()));
|
|
|
|
let codex_home = tempdir().unwrap();
|
|
let mock_server = MockServer::start().await;
|
|
|
|
mock_usercode_failure(&mock_server, /*status*/ 503).await;
|
|
|
|
let issuer = mock_server.uri();
|
|
|
|
let opts = server_opts(&codex_home, issuer, AuthCredentialsStoreMode::File);
|
|
|
|
let err = run_device_code_login(opts)
|
|
.await
|
|
.expect_err("usercode HTTP failure should bubble up");
|
|
assert!(
|
|
err.to_string()
|
|
.contains("device code request failed with status"),
|
|
"unexpected error: {err:?}"
|
|
);
|
|
|
|
let auth = load_auth_dot_json(codex_home.path(), AuthCredentialsStoreMode::File)
|
|
.context("auth.json should load after login fails")?;
|
|
assert!(
|
|
auth.is_none(),
|
|
"auth.json should not be created when login fails"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn device_code_login_integration_persists_without_api_key_on_exchange_failure()
|
|
-> anyhow::Result<()> {
|
|
skip_if_no_network!(Ok(()));
|
|
|
|
let codex_home = tempdir().unwrap();
|
|
|
|
let mock_server = MockServer::start().await;
|
|
|
|
mock_usercode_success(&mock_server).await;
|
|
|
|
mock_poll_token_two_step(
|
|
&mock_server,
|
|
Arc::new(AtomicUsize::new(0)),
|
|
/*first_response_status*/ 404,
|
|
)
|
|
.await;
|
|
|
|
let jwt = make_jwt(json!({}));
|
|
|
|
mock_oauth_token_single(&mock_server, jwt.clone()).await;
|
|
|
|
let issuer = mock_server.uri();
|
|
|
|
let mut opts = ServerOptions::new(
|
|
codex_home.path().to_path_buf(),
|
|
"client-id".to_string(),
|
|
/*forced_chatgpt_workspace_id*/ None,
|
|
AuthCredentialsStoreMode::File,
|
|
);
|
|
opts.issuer = issuer;
|
|
opts.open_browser = false;
|
|
|
|
run_device_code_login(opts)
|
|
.await
|
|
.expect("device login should succeed without API key exchange");
|
|
|
|
let auth = load_auth_dot_json(codex_home.path(), AuthCredentialsStoreMode::File)
|
|
.context("auth.json should load after login succeeds")?
|
|
.context("auth.json written")?;
|
|
assert!(auth.openai_api_key.is_none());
|
|
let tokens = auth.tokens.expect("tokens persisted");
|
|
assert_eq!(tokens.access_token, "access-token-123");
|
|
assert_eq!(tokens.refresh_token, "refresh-token-123");
|
|
assert_eq!(tokens.id_token.raw_jwt, jwt);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn device_code_login_integration_handles_error_payload() -> anyhow::Result<()> {
|
|
skip_if_no_network!(Ok(()));
|
|
|
|
let codex_home = tempdir().unwrap();
|
|
|
|
// Start WireMock
|
|
let mock_server = MockServer::start().await;
|
|
|
|
mock_usercode_success(&mock_server).await;
|
|
|
|
// // /deviceauth/token → returns error payload with status 401
|
|
mock_poll_token_single(
|
|
&mock_server,
|
|
"/api/accounts/deviceauth/token",
|
|
ResponseTemplate::new(401).set_body_json(json!({
|
|
"error": "authorization_declined",
|
|
"error_description": "Denied"
|
|
})),
|
|
)
|
|
.await;
|
|
|
|
// (WireMock will automatically 404 for other paths)
|
|
|
|
let issuer = mock_server.uri();
|
|
|
|
let mut opts = ServerOptions::new(
|
|
codex_home.path().to_path_buf(),
|
|
"client-id".to_string(),
|
|
/*forced_chatgpt_workspace_id*/ None,
|
|
AuthCredentialsStoreMode::File,
|
|
);
|
|
opts.issuer = issuer;
|
|
opts.open_browser = false;
|
|
|
|
let err = run_device_code_login(opts)
|
|
.await
|
|
.expect_err("integration failure path should return error");
|
|
|
|
// Accept either the specific error payload, a 400, or a 404 (since the client may return 404 if the flow is incomplete)
|
|
assert!(
|
|
err.to_string().contains("authorization_declined") || err.to_string().contains("401"),
|
|
"Expected an authorization_declined / 400 / 404 error, got {err:?}"
|
|
);
|
|
|
|
let auth = load_auth_dot_json(codex_home.path(), AuthCredentialsStoreMode::File)
|
|
.context("auth.json should load after login fails")?;
|
|
assert!(
|
|
auth.is_none(),
|
|
"auth.json should not be created when device auth fails"
|
|
);
|
|
Ok(())
|
|
}
|