Files
codex/codex-rs/models-manager/src/cache_identity_tests.rs
Ahmed Ibrahim f046cf35df Scope model catalog caches to the current provider and auth identity (#43906)
## Why

Switching providers or authentication could reuse a previous identity's model catalog, including its default service tier. An in-flight refresh could also overwrite a newer account's catalog after cache storage completed.

## What changed

- Require a matching provider and auth identity for disk and in-memory catalog reuse. Treat legacy entries without an identity as cache misses and fall back to bundled metadata when the in-memory identity no longer matches.
- Recheck identity after fetching and storing a catalog before publishing it.
- Renew cache freshness only when the client version, identity, and ETag match the stored entry.
- Update cache fixtures to include the configured provider and auth identity.

## Testing

Add regression coverage for provider and auth switches, credential rotation, legacy and mismatched cache entries, conditional TTL renewal, and an account switch during cache storage. Verify that API-key requests do not inherit a ChatGPT catalog's default `service_tier`, while explicit tier selection still works.

GitOrigin-RevId: e15d7ffff238d951d7a1c6a68381cbb479994146
2026-09-08 20:37:30 +00:00

84 lines
2.9 KiB
Rust

//! Coverage for cache identity validation and conditional freshness updates.
use super::*;
use pretty_assertions::assert_eq;
#[tokio::test]
async fn mismatched_and_legacy_cache_entries_fetch_the_current_catalog() {
for identity in [None, Some("other-provider-or-account".to_string())] {
let stale = remote_model("stale", "Stale", /*priority*/ 0);
let current = remote_model("current", "Current", /*priority*/ 0);
let cache = TestModelsCache::with_entry(ModelsCacheEntry {
fetched_at: Utc::now(),
etag: Some("old-etag".into()),
client_version: Some(crate::client_version_to_whole()),
identity,
models: vec![stale],
});
let endpoint = TestModelsEndpoint::new(vec![vec![current.clone()]]);
let manager = OpenAiModelsManager::new_with_cache(
cache.clone(),
endpoint.clone(),
Some(AuthManager::from_auth_for_testing(
CodexAuth::create_dummy_chatgpt_auth_for_testing(),
)),
);
assert_eq!(
manager
.raw_model_catalog(
RefreshStrategy::OnlineIfUncached,
DEFAULT_HTTP_CLIENT_FACTORY
)
.await
.models,
vec![current.clone()]
);
assert_eq!(endpoint.fetch_count(), 1);
}
}
#[tokio::test]
async fn matching_etag_cannot_renew_a_different_identity_or_catalog() {
let home = tempdir().unwrap();
let cache = FileModelsCache::new(home.path().join(MODEL_CACHE_FILE), DEFAULT_MODEL_CACHE_TTL);
let expected = ModelsCacheEntry {
fetched_at: Utc::now() - chrono::Duration::hours(1),
etag: Some("etag".into()),
client_version: Some(crate::client_version_to_whole()),
identity: Some("first-account".into()),
models: vec![remote_model("model", "Model", /*priority*/ 0)],
};
for (key, etag, version) in [
("other-account", "etag", crate::client_version_to_whole()),
(
"first-account",
"new-etag",
crate::client_version_to_whole(),
),
("first-account", "etag", "other-version".into()),
] {
let stored = ModelsCacheEntry {
identity: Some(key.into()),
etag: Some(etag.into()),
client_version: Some(version),
..expected.clone()
};
cache.store(&stored).await.unwrap();
cache
.refresh_ttl(
expected.client_version.as_deref().unwrap(),
expected.identity.as_deref().unwrap(),
expected.etag.as_deref().unwrap(),
)
.await
.unwrap();
let actual: ModelsCacheEntry = serde_json::from_slice(
&tokio::fs::read(home.path().join(MODEL_CACHE_FILE))
.await
.unwrap(),
)
.unwrap();
assert_eq!(actual, stored);
}
}