Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:35:30 +08:00

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}"
);
}
}