26382a7ac6
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
JetBrains Plugin / Actionlint (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
CodeQL / Analyze (rust) (push) Waiting to run
JetBrains Plugin / Validation (push) Waiting to run
JetBrains Plugin / Build (push) Waiting to run
JetBrains Plugin / Test (push) Blocked by required conditions
Security Check / Security Scan (push) Waiting to run
CI / Clippy (push) Failing after 15m13s
CI / Test (ubuntu-latest) (push) Failing after 16m1s
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Build (no embeddings / no ORT) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Cookbook (Node) (push) Has been cancelled
CI / Pi Extension (Node) (push) Has been cancelled
CI / Rust SDK (lean-ctx-client) (push) Has been cancelled
CI / Embed SDK (lean-ctx-sdk) (push) Has been cancelled
CI / Python SDK (leanctx) (push) Has been cancelled
CI / Hermes Plugin (Python) (push) Has been cancelled
CI / SDK Conformance Matrix (push) Has been cancelled
CI / Coverage (push) Has been cancelled
CI / cargo-deny (push) Has been cancelled
CI / Adversarial Safety (push) Has been cancelled
CI / Benchmarks (push) Has been cancelled
CI / Output-Quality Gate (eval A/B) (push) Has been cancelled
CI / Documentation (push) Has been cancelled
CI / CI Green (push) Has been cancelled
209 lines
8.5 KiB
Rust
209 lines
8.5 KiB
Rust
//! End-to-end regression for #554 / #597 / #603: `proxy enable` must treat the two
|
|
//! Codex auth modes differently, and a ChatGPT *subscription* login must stay native
|
|
//! unless the user explicitly opts into proxy routing.
|
|
//!
|
|
//! - **API-key** mode is billed per token, so Codex is pointed at the proxy's `/v1`
|
|
//! rail (top-level `openai_base_url`).
|
|
//! - **ChatGPT subscription, default (opt-out)**: left native — no lean-ctx Codex
|
|
//! config — so history and `codex cloud`/remote keep working (#597). Pinning a
|
|
//! `model_provider` scopes Codex history to that provider, which is exactly the
|
|
//! regression #597 reverted, so routing a subscription is opt-in.
|
|
//! - **ChatGPT subscription, opt-in** (`LEAN_CTX_CODEX_CHATGPT_PROXY` set, or
|
|
//! `[proxy] codex_chatgpt_proxy = true`): setup pins the generated
|
|
//! `leanctx-chatgpt` provider so model turns route through the proxy's Codex
|
|
//! backend rail.
|
|
//!
|
|
//! All scenarios live in one serial test: they redirect Codex via `CODEX_HOME`
|
|
//! (the documented override `resolve_codex_dir` honours) and a live dummy proxy so
|
|
//! the reachability guard passes. A single test means no in-process race on the
|
|
//! shared env vars, and a dedicated test binary isolates it from the lib suite.
|
|
|
|
use std::ffi::OsString;
|
|
use std::net::TcpListener;
|
|
use std::path::Path;
|
|
|
|
/// The four XDG category overrides that the crate-private `data_dir::isolated_data_dir()`
|
|
/// sets to isolate a test's config/data/state/cache. Re-declared here because that
|
|
/// helper is `#[cfg(test)]` and therefore unavailable to this integration-test binary.
|
|
const ISOLATED_ENV_VARS: [&str; 4] = [
|
|
"LEAN_CTX_DATA_DIR",
|
|
"LEAN_CTX_CONFIG_DIR",
|
|
"LEAN_CTX_STATE_DIR",
|
|
"LEAN_CTX_CACHE_DIR",
|
|
];
|
|
|
|
/// Scope-guard that points `CODEX_HOME` at `dir` and restores the previous value on
|
|
/// drop. `set_var`/`remove_var` are `unsafe` on edition 2024; safe here because this
|
|
/// test binary runs the single test below serially.
|
|
struct CodexHome(Option<OsString>);
|
|
|
|
impl CodexHome {
|
|
fn set(dir: &Path) -> Self {
|
|
let prev = std::env::var_os("CODEX_HOME");
|
|
unsafe { std::env::set_var("CODEX_HOME", dir) };
|
|
CodexHome(prev)
|
|
}
|
|
}
|
|
|
|
impl Drop for CodexHome {
|
|
fn drop(&mut self) {
|
|
match &self.0 {
|
|
Some(v) => unsafe { std::env::set_var("CODEX_HOME", v) },
|
|
None => unsafe { std::env::remove_var("CODEX_HOME") },
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Scope-guard for an arbitrary env var that restores the previous value on drop.
|
|
/// Used to flip the `LEAN_CTX_CODEX_CHATGPT_PROXY` opt-in per scenario without
|
|
/// leaking into the next one. Same `unsafe`/serial-test caveat as [`CodexHome`].
|
|
struct EnvVar {
|
|
key: &'static str,
|
|
prev: Option<OsString>,
|
|
}
|
|
|
|
impl EnvVar {
|
|
fn set(key: &'static str, value: &str) -> Self {
|
|
let prev = std::env::var_os(key);
|
|
unsafe { std::env::set_var(key, value) };
|
|
EnvVar { key, prev }
|
|
}
|
|
|
|
fn cleared(key: &'static str) -> Self {
|
|
let prev = std::env::var_os(key);
|
|
unsafe { std::env::remove_var(key) };
|
|
EnvVar { key, prev }
|
|
}
|
|
}
|
|
|
|
impl Drop for EnvVar {
|
|
fn drop(&mut self) {
|
|
match &self.prev {
|
|
Some(v) => unsafe { std::env::set_var(self.key, v) },
|
|
None => unsafe { std::env::remove_var(self.key) },
|
|
}
|
|
}
|
|
}
|
|
|
|
fn dummy_proxy_port() -> (TcpListener, u16) {
|
|
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
|
let port = listener.local_addr().unwrap().port();
|
|
(listener, port)
|
|
}
|
|
|
|
/// Writes a Codex `.codex` dir with the given auth mode and an unrelated config key,
|
|
/// then returns the temp dir (kept alive by the caller) and the `.codex` path.
|
|
fn codex_home_with_auth(auth_json: &str) -> (tempfile::TempDir, std::path::PathBuf) {
|
|
let home = tempfile::tempdir().unwrap();
|
|
let codex = home.path().join(".codex");
|
|
std::fs::create_dir_all(&codex).unwrap();
|
|
std::fs::write(codex.join("auth.json"), auth_json).unwrap();
|
|
std::fs::write(codex.join("config.toml"), "model = \"gpt-5.5\"\n").unwrap();
|
|
(home, codex)
|
|
}
|
|
|
|
#[test]
|
|
fn proxy_enable_respects_codex_auth_mode_554() {
|
|
// An explicit OPENAI_API_KEY forces API-key mode regardless of auth.json, which
|
|
// would invalidate the ChatGPT-login halves of this test.
|
|
if std::env::var("OPENAI_API_KEY").is_ok_and(|v| !v.trim().is_empty()) {
|
|
return;
|
|
}
|
|
|
|
// Hermetic config isolation. The ChatGPT-proxy opt-in is resolved from the global
|
|
// `[proxy] codex_chatgpt_proxy` (env-independent, #603/#616), so without this a
|
|
// developer who enabled it would see the default opt-out scenario observe an
|
|
// opt-in it never set. Integration tests cannot use the crate-private
|
|
// `isolated_data_dir()` (`#[cfg(test)]`), so replicate it: point all four XDG
|
|
// category overrides at a throwaway dir. This binary runs one serial test in its
|
|
// own process, so mutating process env here is private and safe.
|
|
let data_home = tempfile::tempdir().unwrap();
|
|
let _iso: Vec<EnvVar> = ISOLATED_ENV_VARS
|
|
.iter()
|
|
.copied()
|
|
.map(|key| EnvVar::set(key, &data_home.path().to_string_lossy()))
|
|
.collect();
|
|
|
|
const CHATGPT_AUTH: &str = r#"{"auth_mode":"chatgpt","tokens":{"access_token":"x"}}"#;
|
|
|
|
// --- ChatGPT login, default (opt-out): stay native, write no proxy entries (#597).
|
|
{
|
|
let _no_optin = EnvVar::cleared("LEAN_CTX_CODEX_CHATGPT_PROXY");
|
|
let (home, codex) = codex_home_with_auth(CHATGPT_AUTH);
|
|
let _codex_home = CodexHome::set(&codex);
|
|
let (_listener, port) = dummy_proxy_port();
|
|
lean_ctx::proxy_setup::install_proxy_env_unchecked(home.path(), port, true, false);
|
|
|
|
let cfg = std::fs::read_to_string(codex.join("config.toml")).unwrap();
|
|
assert!(
|
|
!cfg.contains("model_provider = \"leanctx-chatgpt\""),
|
|
"default ChatGPT login must stay native — no model_provider pin (#597), got:\n{cfg}"
|
|
);
|
|
assert!(
|
|
!cfg.contains("chatgpt_base_url"),
|
|
"default ChatGPT login must write no proxy rail, got:\n{cfg}"
|
|
);
|
|
assert!(
|
|
!cfg.contains("openai_base_url"),
|
|
"a ChatGPT login must never use the OpenAI API-key /v1 rail, got:\n{cfg}"
|
|
);
|
|
assert!(
|
|
cfg.contains("model = \"gpt-5.5\""),
|
|
"unrelated Codex config must be preserved, got:\n{cfg}"
|
|
);
|
|
}
|
|
|
|
// --- ChatGPT login, opt-in: pin the lean-ctx ChatGPT provider + backend rail.
|
|
{
|
|
let _optin = EnvVar::set("LEAN_CTX_CODEX_CHATGPT_PROXY", "1");
|
|
let (home, codex) = codex_home_with_auth(CHATGPT_AUTH);
|
|
let _codex_home = CodexHome::set(&codex);
|
|
let (_listener, port) = dummy_proxy_port();
|
|
lean_ctx::proxy_setup::install_proxy_env_unchecked(home.path(), port, true, false);
|
|
|
|
let cfg = std::fs::read_to_string(codex.join("config.toml")).unwrap();
|
|
assert!(
|
|
cfg.contains("model_provider = \"leanctx-chatgpt\""),
|
|
"opt-in ChatGPT login must select the lean-ctx ChatGPT provider, got:\n{cfg}"
|
|
);
|
|
assert!(
|
|
!cfg.contains("chatgpt_base_url"),
|
|
"stale ChatGPT aux/app routing must be removed, got:\n{cfg}"
|
|
);
|
|
assert!(
|
|
cfg.contains(&format!(
|
|
"base_url = \"http://127.0.0.1:{port}/backend-api/codex\""
|
|
)),
|
|
"ChatGPT provider block must point model turns at backend-api/codex, got:\n{cfg}"
|
|
);
|
|
assert!(
|
|
!cfg.contains("openai_base_url"),
|
|
"opt-in ChatGPT login must not use the OpenAI API-key /v1 rail, got:\n{cfg}"
|
|
);
|
|
assert!(
|
|
cfg.contains("model = \"gpt-5.5\""),
|
|
"unrelated Codex config must be preserved, got:\n{cfg}"
|
|
);
|
|
}
|
|
|
|
// --- API-key login: Codex is pointed at the proxy via top-level openai_base_url.
|
|
{
|
|
let _no_optin = EnvVar::cleared("LEAN_CTX_CODEX_CHATGPT_PROXY");
|
|
let (home, codex) =
|
|
codex_home_with_auth(r#"{"auth_mode":"apikey","OPENAI_API_KEY":"sk-test"}"#);
|
|
let _codex_home = CodexHome::set(&codex);
|
|
let (_listener, port) = dummy_proxy_port();
|
|
lean_ctx::proxy_setup::install_proxy_env_unchecked(home.path(), port, true, false);
|
|
|
|
let cfg = std::fs::read_to_string(codex.join("config.toml")).unwrap();
|
|
assert!(
|
|
cfg.contains(&format!("openai_base_url = \"http://127.0.0.1:{port}/v1\"")),
|
|
"API-key Codex must be pointed at the proxy via top-level openai_base_url, got:\n{cfg}"
|
|
);
|
|
assert!(
|
|
cfg.contains("model = \"gpt-5.5\""),
|
|
"unrelated Codex config must be preserved, got:\n{cfg}"
|
|
);
|
|
}
|
|
}
|