mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
e7039f9844
## Summary - Splits the monolithic `codex-cloud-config` implementation into focused modules. - Keeps behavior unchanged from the preceding config bundle runtime switch. ## Details This is the reviewability follow-up after the lineage-preserving migration PRs. The split separates backend transport, loader construction, cache handling, metrics, validation, service orchestration, and focused tests into named files. Verification: `just fmt`; `just test -p codex-cloud-config`.
207 lines
6.8 KiB
Rust
207 lines
6.8 KiB
Rust
use super::*;
|
|
use codex_config::AbsolutePathBuf;
|
|
use codex_config::CloudConfigFragment;
|
|
use codex_config::CloudConfigTomlBundle;
|
|
use codex_config::CloudRequirementsFragment;
|
|
use codex_config::CloudRequirementsTomlBundle;
|
|
use pretty_assertions::assert_eq;
|
|
use std::path::Path;
|
|
use tempfile::tempdir;
|
|
|
|
fn test_bundle() -> CloudConfigBundle {
|
|
CloudConfigBundle {
|
|
config_toml: CloudConfigTomlBundle {
|
|
enterprise_managed: vec![CloudConfigFragment {
|
|
id: "cfg_1".to_string(),
|
|
name: "Base config".to_string(),
|
|
contents: "model = \"gpt-5\"".to_string(),
|
|
}],
|
|
},
|
|
requirements_toml: CloudRequirementsTomlBundle {
|
|
enterprise_managed: vec![CloudRequirementsFragment {
|
|
id: "req_1".to_string(),
|
|
name: "Base requirements".to_string(),
|
|
contents: "allowed_approval_policies = [\"never\"]".to_string(),
|
|
}],
|
|
},
|
|
}
|
|
}
|
|
|
|
fn signed_cache_file(
|
|
signed_payload: CloudConfigBundleCacheSignedPayload,
|
|
) -> CloudConfigBundleCacheFile {
|
|
let payload_bytes = cache_payload_bytes(&signed_payload).expect("payload bytes");
|
|
CloudConfigBundleCacheFile {
|
|
signature: sign_cache_payload(&payload_bytes).expect("signature"),
|
|
signed_payload,
|
|
}
|
|
}
|
|
|
|
fn valid_signed_payload() -> CloudConfigBundleCacheSignedPayload {
|
|
let cached_at = Utc::now();
|
|
CloudConfigBundleCacheSignedPayload {
|
|
version: CLOUD_CONFIG_BUNDLE_CACHE_VERSION,
|
|
cached_at,
|
|
expires_at: cached_at + ChronoDuration::minutes(30),
|
|
chatgpt_user_id: Some("user-12345".to_string()),
|
|
account_id: Some("account-12345".to_string()),
|
|
bundle: test_bundle(),
|
|
}
|
|
}
|
|
|
|
fn write_cache_file(cache: &CloudConfigBundleCache, cache_file: &CloudConfigBundleCacheFile) {
|
|
std::fs::write(
|
|
cache.path(),
|
|
serde_json::to_vec_pretty(cache_file).expect("serialize cache"),
|
|
)
|
|
.expect("write cache");
|
|
}
|
|
|
|
fn create_test_cache(codex_home: &Path) -> CloudConfigBundleCache {
|
|
CloudConfigBundleCache::new(AbsolutePathBuf::resolve_path_against_base(codex_home, "/"))
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn save_writes_signed_payload_and_loads_for_matching_identity() {
|
|
let codex_home = tempdir().expect("tempdir");
|
|
let cache = create_test_cache(codex_home.path());
|
|
let bundle = test_bundle();
|
|
|
|
cache
|
|
.save(
|
|
Some("user-12345".to_string()),
|
|
Some("account-12345".to_string()),
|
|
bundle.clone(),
|
|
)
|
|
.await
|
|
.expect("save cache");
|
|
|
|
let cache_file: CloudConfigBundleCacheFile =
|
|
serde_json::from_slice(&std::fs::read(cache.path()).expect("read cache"))
|
|
.expect("parse cache");
|
|
assert!(
|
|
cache_file.signed_payload.expires_at
|
|
<= cache_file.signed_payload.cached_at + ChronoDuration::minutes(30)
|
|
);
|
|
assert!(cache_file.signed_payload.expires_at > cache_file.signed_payload.cached_at);
|
|
assert_eq!(
|
|
cache_file,
|
|
signed_cache_file(CloudConfigBundleCacheSignedPayload {
|
|
version: CLOUD_CONFIG_BUNDLE_CACHE_VERSION,
|
|
cached_at: cache_file.signed_payload.cached_at,
|
|
expires_at: cache_file.signed_payload.expires_at,
|
|
chatgpt_user_id: Some("user-12345".to_string()),
|
|
account_id: Some("account-12345".to_string()),
|
|
bundle,
|
|
})
|
|
);
|
|
|
|
assert_eq!(
|
|
cache.load(Some("user-12345"), Some("account-12345")).await,
|
|
Ok(cache_file.signed_payload)
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn load_rejects_missing_request_identity_before_reading_cache_file() {
|
|
let codex_home = tempdir().expect("tempdir");
|
|
let cache = create_test_cache(codex_home.path());
|
|
|
|
assert_eq!(
|
|
cache
|
|
.load(/*chatgpt_user_id*/ None, Some("account-12345"))
|
|
.await,
|
|
Err(CacheLoadStatus::AuthIdentityIncomplete)
|
|
);
|
|
assert_eq!(
|
|
cache.load(Some("user-12345"), /*account_id*/ None).await,
|
|
Err(CacheLoadStatus::AuthIdentityIncomplete)
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn load_reports_missing_and_malformed_cache_files() {
|
|
let codex_home = tempdir().expect("tempdir");
|
|
let cache = create_test_cache(codex_home.path());
|
|
|
|
assert_eq!(
|
|
cache.load(Some("user-12345"), Some("account-12345")).await,
|
|
Err(CacheLoadStatus::CacheFileNotFound)
|
|
);
|
|
|
|
std::fs::write(cache.path(), "{").expect("write malformed cache");
|
|
assert!(matches!(
|
|
cache.load(Some("user-12345"), Some("account-12345")).await,
|
|
Err(CacheLoadStatus::CacheParseFailed(_))
|
|
));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn load_rejects_tampered_payload() {
|
|
let codex_home = tempdir().expect("tempdir");
|
|
let cache = create_test_cache(codex_home.path());
|
|
let mut cache_file = signed_cache_file(valid_signed_payload());
|
|
cache_file
|
|
.signed_payload
|
|
.bundle
|
|
.requirements_toml
|
|
.enterprise_managed[0]
|
|
.contents = "allowed_approval_policies = [\"on-request\"]".to_string();
|
|
write_cache_file(&cache, &cache_file);
|
|
|
|
assert_eq!(
|
|
cache.load(Some("user-12345"), Some("account-12345")).await,
|
|
Err(CacheLoadStatus::CacheSignatureInvalid)
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn load_rejects_cache_for_incomplete_or_different_identity() {
|
|
let codex_home = tempdir().expect("tempdir");
|
|
let cache = create_test_cache(codex_home.path());
|
|
let cache_file = signed_cache_file(valid_signed_payload());
|
|
write_cache_file(&cache, &cache_file);
|
|
|
|
assert_eq!(
|
|
cache.load(Some("user-99999"), Some("account-12345")).await,
|
|
Err(CacheLoadStatus::CacheIdentityMismatch)
|
|
);
|
|
|
|
let mut signed_payload = valid_signed_payload();
|
|
signed_payload.chatgpt_user_id = None;
|
|
write_cache_file(&cache, &signed_cache_file(signed_payload));
|
|
|
|
assert_eq!(
|
|
cache.load(Some("user-12345"), Some("account-12345")).await,
|
|
Err(CacheLoadStatus::CacheIdentityIncomplete)
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn load_rejects_expired_cache() {
|
|
let codex_home = tempdir().expect("tempdir");
|
|
let cache = create_test_cache(codex_home.path());
|
|
let mut signed_payload = valid_signed_payload();
|
|
signed_payload.expires_at = Utc::now() - ChronoDuration::seconds(1);
|
|
write_cache_file(&cache, &signed_cache_file(signed_payload));
|
|
|
|
assert_eq!(
|
|
cache.load(Some("user-12345"), Some("account-12345")).await,
|
|
Err(CacheLoadStatus::CacheExpired)
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn load_rejects_unsupported_cache_version() {
|
|
let codex_home = tempdir().expect("tempdir");
|
|
let cache = create_test_cache(codex_home.path());
|
|
let mut signed_payload = valid_signed_payload();
|
|
signed_payload.version = 2;
|
|
write_cache_file(&cache, &signed_cache_file(signed_payload));
|
|
|
|
assert_eq!(
|
|
cache.load(Some("user-12345"), Some("account-12345")).await,
|
|
Err(CacheLoadStatus::CacheVersionUnsupported(2))
|
|
);
|
|
}
|