Files
codex/codex-rs/app-server-protocol/src/protocol/common_tests.rs
T
dand-oai 01ab354312 Expose ChatGPT user IDs in account/read responses (#60004)
## What changed

Add a nullable `userId` to `GetAccountResponse`, populated from the same authenticated account snapshot as the rest of the response. ChatGPT and personal access token sessions return the canonical ChatGPT user ID when available; API-only and signed-out sessions return `null`.

Update the cloud client, generated schemas, TypeScript and Python types, and account identity documentation. Default missing IDs to `None` when deserializing responses from older app servers.

## Testing

Extend account response tests to check ChatGPT and personal access token user IDs, null IDs for other providers and signed-out sessions, and ID changes when switching providers with saved ChatGPT credentials.

GitOrigin-RevId: 7ad3da3cfc835c4d08d03c8e91b06cd7e007d874
2026-11-06 22:27:47 +00:00

146 lines
4.9 KiB
Rust

use super::*;
use anyhow::Result;
use codex_protocol::protocol::TurnAbortReason;
use pretty_assertions::assert_eq;
use serde_json::json;
#[test]
fn client_response_payload_serializes_without_an_intermediate_json_value() -> Result<()> {
let payload = ClientResponsePayload::ThreadArchive(v2::ThreadArchiveResponse {});
assert_eq!(serde_json::to_string(&payload)?, "{}");
let Some(ClientResponse::ThreadArchive {
request_id,
response: _,
}) = payload.into_client_response(RequestId::Integer(7))
else {
panic!("expected thread/archive client response");
};
assert_eq!(request_id, RequestId::Integer(7));
Ok(())
}
#[test]
fn interrupt_conversation_payload_stays_jsonrpc_only() -> Result<()> {
let payload = ClientResponsePayload::InterruptConversation(v1::InterruptConversationResponse {
abort_reason: TurnAbortReason::Interrupted,
});
assert_eq!(
serde_json::to_value(&payload)?,
json!({
"abortReason": "interrupted",
})
);
assert!(
payload
.into_client_response(RequestId::Integer(8))
.is_none()
);
Ok(())
}
#[test]
fn client_response_jsonrpc_parts_preserve_payloads_and_request_ids() -> Result<()> {
let payloads = [
(
ClientResponsePayload::GetAuthStatus(v1::GetAuthStatusResponse {
auth_method: Some(AuthMode::Chatgpt),
auth_token: None,
requires_openai_auth: Some(true),
}),
json!({
"authMethod": "chatgpt",
"authToken": null,
"requiresOpenaiAuth": true,
}),
),
(
ClientResponsePayload::GetAccount(v2::GetAccountResponse {
user_id: None,
account: Some(v2::Account::ApiKey {}),
requires_openai_auth: false,
workspace_routing: None,
}),
json!({
"account": { "type": "apiKey" },
"userId": null,
"requiresOpenaiAuth": false,
"workspaceRouting": null,
}),
),
];
for (payload, expected_result) in payloads {
for request_id in [RequestId::Integer(7), RequestId::String("request-7".into())] {
let expected = (request_id.clone(), expected_result.clone());
let response = payload
.clone()
.into_client_response(request_id.clone())
.expect("request-backed payload has a typed response");
assert_eq!(response.into_jsonrpc_parts()?, expected);
assert_eq!(payload.to_jsonrpc_parts(request_id.clone())?, expected);
assert_eq!(payload.clone().into_jsonrpc_parts(request_id)?, expected);
}
}
Ok(())
}
#[test]
fn client_response_jsonrpc_parts_preserve_legacy_interrupt() -> Result<()> {
let request_id = RequestId::String("interrupt-7".into());
let payload = ClientResponsePayload::InterruptConversation(v1::InterruptConversationResponse {
abort_reason: TurnAbortReason::Interrupted,
});
let expected = (request_id.clone(), json!({ "abortReason": "interrupted" }));
assert!(
payload
.clone()
.into_client_response(request_id.clone())
.is_none()
);
assert_eq!(payload.to_jsonrpc_parts(request_id.clone())?, expected);
assert_eq!(payload.into_jsonrpc_parts(request_id)?, expected);
Ok(())
}
#[cfg(unix)]
#[test]
fn client_response_jsonrpc_parts_preserve_serialization_errors() {
use std::ffi::OsString;
use std::os::unix::ffi::OsStringExt;
let request_id = RequestId::Integer(7);
let response = v1::GetConversationSummaryResponse {
summary: v1::ConversationSummary {
conversation_id: codex_protocol::ThreadId::from_u128(/*value*/ 7),
path: PathBuf::from(OsString::from_vec(vec![0xff])),
preview: String::new(),
timestamp: None,
updated_at: None,
model_provider: String::new(),
cwd: PathBuf::new(),
cli_version: String::new(),
source: codex_protocol::protocol::SessionSource::Exec,
git_info: None,
},
};
let describe = |error: serde_json::Error| (error.classify(), error.to_string());
let expected = describe(serde_json::to_value(&response).unwrap_err());
let payload = ClientResponsePayload::GetConversationSummary(response.clone());
let typed = ClientResponse::GetConversationSummary {
request_id: request_id.clone(),
response,
};
assert_eq!(describe(typed.into_jsonrpc_parts().unwrap_err()), expected);
assert_eq!(
describe(payload.to_jsonrpc_parts(request_id.clone()).unwrap_err()),
expected
);
assert_eq!(
describe(payload.into_jsonrpc_parts(request_id).unwrap_err()),
expected
);
}