Files
yvgude--lean-ctx/rust/tests/proxy_stale_env_scenarios.rs
T
wehub-resource-sync 26382a7ac6
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
JetBrains Plugin / Actionlint (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (rust) (push) Has been cancelled
JetBrains Plugin / Validation (push) Has been cancelled
JetBrains Plugin / Build (push) Has been cancelled
JetBrains Plugin / Test (push) Has been cancelled
Security Check / Security Scan (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:30 +08:00

274 lines
10 KiB
Rust

//! Scenario tests for GitHub Issue #256:
//! Stale `ANTHROPIC_BASE_URL` detection and cleanup when proxy is not enabled.
//!
//! Each test uses an isolated temp dir for both `LEAN_CTX_DATA_DIR` (config)
//! and `CLAUDE_CONFIG_DIR` (Claude Code settings) to avoid interference
//! with the host system's real config.
struct TestEnv {
_tmp: tempfile::TempDir,
home: std::path::PathBuf,
}
impl TestEnv {
fn new() -> Self {
let tmp = tempfile::tempdir().unwrap();
let home = tmp.path().to_path_buf();
let data_dir = home.join(".lean-ctx");
std::fs::create_dir_all(&data_dir).unwrap();
// TODO: Audit that the environment access only happens in single-threaded code.
unsafe { std::env::set_var("LEAN_CTX_DATA_DIR", &data_dir) };
// TODO: Audit that the environment access only happens in single-threaded code.
unsafe { std::env::set_var("CLAUDE_CONFIG_DIR", home.join(".claude")) };
// TODO: Audit that the environment access only happens in single-threaded code.
unsafe { std::env::set_var("CODEX_HOME", home.join(".codex")) };
Self { _tmp: tmp, home }
}
fn set_claude_settings(&self, json: &str) {
let claude_dir = self.home.join(".claude");
std::fs::create_dir_all(&claude_dir).unwrap();
std::fs::write(claude_dir.join("settings.json"), json).unwrap();
}
fn read_claude_settings(&self) -> serde_json::Value {
let path = self.home.join(".claude/settings.json");
let content = std::fs::read_to_string(path).unwrap_or_default();
serde_json::from_str(&content).unwrap_or(serde_json::json!({}))
}
}
impl Drop for TestEnv {
fn drop(&mut self) {
// TODO: Audit that the environment access only happens in single-threaded code.
unsafe { std::env::remove_var("LEAN_CTX_DATA_DIR") };
// TODO: Audit that the environment access only happens in single-threaded code.
unsafe { std::env::remove_var("CLAUDE_CONFIG_DIR") };
// TODO: Audit that the environment access only happens in single-threaded code.
unsafe { std::env::remove_var("CODEX_HOME") };
}
}
// ---------------------------------------------------------------------------
// Scenario 1: is_local_lean_ctx_url correctly identifies local proxy URLs
// ---------------------------------------------------------------------------
#[test]
fn scenario_local_url_detection() {
use lean_ctx::proxy_setup::is_local_lean_ctx_url;
assert!(is_local_lean_ctx_url("http://127.0.0.1:4444"));
assert!(is_local_lean_ctx_url("http://localhost:4444"));
assert!(is_local_lean_ctx_url("http://127.0.0.1:5555"));
assert!(is_local_lean_ctx_url("http://localhost:3333"));
assert!(!is_local_lean_ctx_url("https://api.anthropic.com"));
assert!(!is_local_lean_ctx_url("https://proxy.company.com:4444"));
assert!(!is_local_lean_ctx_url(""));
assert!(!is_local_lean_ctx_url("http://192.168.1.1:4444"));
}
// ---------------------------------------------------------------------------
// Scenario 2: cleanup_stale_proxy_env removes local URL when proxy disabled
// ---------------------------------------------------------------------------
#[test]
#[serial_test::serial]
fn scenario_cleanup_removes_stale_url() {
let env = TestEnv::new();
env.set_claude_settings(r#"{"env": {"ANTHROPIC_BASE_URL": "http://127.0.0.1:4444"}}"#);
let cleaned = lean_ctx::proxy_setup::cleanup_stale_proxy_env(&env.home);
assert!(cleaned > 0, "should have cleaned stale URL");
let doc = env.read_claude_settings();
let has_url = doc
.get("env")
.and_then(|e| e.get("ANTHROPIC_BASE_URL"))
.is_some();
assert!(!has_url, "ANTHROPIC_BASE_URL should be removed");
}
// ---------------------------------------------------------------------------
// Scenario 3: cleanup does nothing when no stale URL exists
// ---------------------------------------------------------------------------
#[test]
#[serial_test::serial]
fn scenario_cleanup_noop_when_no_stale_url() {
let env = TestEnv::new();
env.set_claude_settings(r#"{"env": {"SOME_OTHER_VAR": "value"}}"#);
let cleaned = lean_ctx::proxy_setup::cleanup_stale_proxy_env(&env.home);
assert_eq!(cleaned, 0, "nothing to clean");
}
// ---------------------------------------------------------------------------
// Scenario 4: cleanup preserves non-local ANTHROPIC_BASE_URL
// ---------------------------------------------------------------------------
#[test]
#[serial_test::serial]
fn scenario_cleanup_preserves_remote_url() {
let env = TestEnv::new();
env.set_claude_settings(r#"{"env": {"ANTHROPIC_BASE_URL": "https://api.anthropic.com"}}"#);
let cleaned = lean_ctx::proxy_setup::cleanup_stale_proxy_env(&env.home);
assert_eq!(cleaned, 0, "should not touch remote URL");
let doc = env.read_claude_settings();
let url = doc
.get("env")
.and_then(|e| e.get("ANTHROPIC_BASE_URL"))
.and_then(|v| v.as_str())
.unwrap_or("");
assert_eq!(
url, "https://api.anthropic.com",
"remote URL must be preserved"
);
}
// ---------------------------------------------------------------------------
// Scenario 5: cleanup handles missing settings.json gracefully
// ---------------------------------------------------------------------------
#[test]
#[serial_test::serial]
fn scenario_cleanup_missing_settings_file() {
let env = TestEnv::new();
let cleaned = lean_ctx::proxy_setup::cleanup_stale_proxy_env(&env.home);
assert_eq!(cleaned, 0, "should handle missing file gracefully");
}
// ---------------------------------------------------------------------------
// Scenario 6: cleanup handles malformed JSON gracefully
// ---------------------------------------------------------------------------
#[test]
#[serial_test::serial]
fn scenario_cleanup_malformed_json() {
let env = TestEnv::new();
env.set_claude_settings("this is not json {{{");
let cleaned = lean_ctx::proxy_setup::cleanup_stale_proxy_env(&env.home);
assert_eq!(cleaned, 0, "should handle malformed JSON gracefully");
}
// ---------------------------------------------------------------------------
// Scenario 7: cleanup handles empty env object
// ---------------------------------------------------------------------------
#[test]
#[serial_test::serial]
fn scenario_cleanup_empty_env_object() {
let env = TestEnv::new();
env.set_claude_settings(r#"{"env": {}}"#);
let cleaned = lean_ctx::proxy_setup::cleanup_stale_proxy_env(&env.home);
assert_eq!(cleaned, 0, "nothing to clean with empty env");
}
// ---------------------------------------------------------------------------
// Scenario 8: cleanup with localhost variant
// ---------------------------------------------------------------------------
#[test]
#[serial_test::serial]
fn scenario_cleanup_localhost_variant() {
let env = TestEnv::new();
env.set_claude_settings(r#"{"env": {"ANTHROPIC_BASE_URL": "http://localhost:4444"}}"#);
let cleaned = lean_ctx::proxy_setup::cleanup_stale_proxy_env(&env.home);
assert!(cleaned > 0, "should clean localhost variant too");
}
// ---------------------------------------------------------------------------
// Scenario 9: cleanup preserves other env vars and settings
// ---------------------------------------------------------------------------
#[test]
#[serial_test::serial]
fn scenario_cleanup_preserves_other_env_vars() {
let env = TestEnv::new();
env.set_claude_settings(
r#"{"env": {"ANTHROPIC_BASE_URL": "http://127.0.0.1:4444", "OTHER_VAR": "keep_me"}, "hooks": {"PreToolUse": []}}"#,
);
let cleaned = lean_ctx::proxy_setup::cleanup_stale_proxy_env(&env.home);
assert!(cleaned > 0);
let doc = env.read_claude_settings();
let other = doc
.get("env")
.and_then(|e| e.get("OTHER_VAR"))
.and_then(|v| v.as_str())
.unwrap_or("");
assert_eq!(other, "keep_me", "other env vars must be preserved");
assert!(doc.get("hooks").is_some(), "hooks must be preserved");
}
// ---------------------------------------------------------------------------
// Scenario 10: has_stale_proxy_url returns correct values
// ---------------------------------------------------------------------------
#[test]
#[serial_test::serial]
fn scenario_has_stale_url_detection() {
let env = TestEnv::new();
assert!(
!lean_ctx::proxy_setup::has_stale_proxy_url(&env.home),
"no file = no stale URL"
);
env.set_claude_settings(r#"{"env": {"ANTHROPIC_BASE_URL": "https://api.anthropic.com"}}"#);
assert!(
!lean_ctx::proxy_setup::has_stale_proxy_url(&env.home),
"remote URL is not stale"
);
env.set_claude_settings(r#"{"env": {"ANTHROPIC_BASE_URL": "http://127.0.0.1:4444"}}"#);
assert!(
lean_ctx::proxy_setup::has_stale_proxy_url(&env.home),
"local URL with proxy disabled is stale"
);
}
// ---------------------------------------------------------------------------
// Scenario 11: cleanup with non-default port
// ---------------------------------------------------------------------------
#[test]
#[serial_test::serial]
fn scenario_cleanup_non_default_port() {
let env = TestEnv::new();
env.set_claude_settings(r#"{"env": {"ANTHROPIC_BASE_URL": "http://127.0.0.1:5555"}}"#);
let cleaned = lean_ctx::proxy_setup::cleanup_stale_proxy_env(&env.home);
assert!(cleaned > 0, "should clean non-default port too");
}
// ---------------------------------------------------------------------------
// Scenario 12: install_proxy_env guard prevents writing when proxy not enabled
// ---------------------------------------------------------------------------
#[test]
#[serial_test::serial]
fn scenario_install_guard_prevents_writing_when_disabled() {
let env = TestEnv::new();
env.set_claude_settings(r"{}");
lean_ctx::proxy_setup::install_proxy_env(&env.home, 4444, true);
let doc = env.read_claude_settings();
let has_url = doc
.get("env")
.and_then(|e| e.get("ANTHROPIC_BASE_URL"))
.is_some();
assert!(
!has_url,
"install_proxy_env should not write URL when proxy is not enabled"
);
}