Files
Kigi-CLI/crates/codegen/kigi-shell/tests/test_settings_refresh.rs
T
ZacharyZhang-NY 021b82443d M1/F1: Kimi Code OAuth device-code flow
Replace the xAI OAuth stack with the Kimi device authorization grant:
- kimi_oauth.rs wire layer (device_authorization + token poll + refresh
  against kigi_env::oauth_host(); client_id per PRD; retryable statuses
  429/5xx with backoff; expired_token restarts authorization)
- X-Msh-Device-{Name,Model,Id} headers; device_id minted uuid4-hex at
  ~/.kigi/device_id (0600)
- Storage: system keyring service `kigi`, entry `oauth/kimi-code`
  (macOS/Windows native backends), atomic-file fallback under ~/.kigi;
  official client's keyring/~/.kimi never touched
- Refresh manager: 60s tick, threshold max(300, expires_in*0.5),
  401-tombstone keyed by rejected refresh token with 300s cooldown and
  rotation auto-clear, cross-process lock with sibling-adoption
  triple-check, sleep/wake forced refresh
- Deleted xAI machinery: enterprise OIDC (PKCE/JWKS/teams), devbox login,
  external auth provider, JWT tier gating + subscription paywall stack,
  X-XAI-Token-Auth marker headers, ZDR gates, /user enrichment
- kigi login / TUI /login both drive the device flow; login-host display
  now derives from kigi_env::oauth_host()
- 264 auth unit/wiremock tests; live contract probe of
  auth.kimi.com/api/oauth/device_authorization matches the wire shapes

Gates: check/clippy --all-targets clean, fmt, deny ok, kigi-shell lib
5131 tests green.
2026-07-17 07:37:29 -04:00

259 lines
8.8 KiB
Rust

//! Integration test: MockInferenceServer `/v1/settings` endpoint and
//! remote settings settings refresh infrastructure.
//!
//! Tests the mock endpoint directly (no binary needed) and verifies
//! the `fetch_settings_blocking` client round-trips correctly with
//! runtime-mutated mock settings.
//!
//! Run locally:
//! ```bash
//! cargo test -p kigi-shell --test test_settings_refresh
//! ```
use std::future::Future;
use kigi_shell::util::config::RemoteSettings;
use kigi_test_support::*;
async fn with_local_set<F, Fut>(f: F)
where
F: FnOnce() -> Fut,
Fut: Future<Output = ()>,
{
tokio::task::LocalSet::new().run_until(f()).await;
}
/// Verify the mock `/v1/settings` endpoint returns 404 when no settings
/// are configured (the default). This preserves backward compatibility:
/// existing tests that never call `set_settings` see a 404, and
/// `fetch_settings_blocking` returns `None`.
#[tokio::test]
async fn test_settings_endpoint_returns_404_when_unconfigured() {
with_local_set(|| async {
let server = MockInferenceServer::start()
.await
.expect("start mock server");
let resp = reqwest::get(format!("{}/settings", server.url()))
.await
.expect("request failed");
assert_eq!(resp.status(), 404);
})
.await;
}
/// Verify the mock `/v1/settings` endpoint returns configured settings
/// and that `set_settings` runtime mutation is reflected immediately.
#[tokio::test]
async fn test_settings_endpoint_returns_configured_settings() {
with_local_set(|| async {
let server = MockInferenceServer::start()
.await
.expect("start mock server");
// Configure initial settings
server.set_settings(RemoteSettings {
tips: Some(vec!["tip_v1".into()]),
leader_mode: Some(false),
..Default::default()
});
// Fetch and verify
let resp = reqwest::get(format!("{}/settings", server.url()))
.await
.expect("request failed");
assert_eq!(resp.status(), 200);
let settings: RemoteSettings = resp.json().await.expect("parse failed");
assert_eq!(settings.tips, Some(vec!["tip_v1".into()]));
assert_eq!(settings.leader_mode, Some(false));
})
.await;
}
/// Verify that `set_settings` updates are visible to subsequent requests
/// (runtime mutation for multi-session test scenarios).
#[tokio::test]
async fn test_settings_endpoint_reflects_runtime_mutations() {
with_local_set(|| async {
let server = MockInferenceServer::start()
.await
.expect("start mock server");
// Initial settings
server.set_settings(RemoteSettings {
tips: Some(vec!["tip_v1".into()]),
..Default::default()
});
let settings: RemoteSettings = reqwest::get(format!("{}/settings", server.url()))
.await
.unwrap()
.json()
.await
.unwrap();
assert_eq!(settings.tips, Some(vec!["tip_v1".into()]));
// Mutate settings (simulating a remote feature flag change)
server.set_settings(RemoteSettings {
tips: Some(vec!["tip_v2".into()]),
leader_mode: Some(true),
..Default::default()
});
// Subsequent request sees the updated values
let settings: RemoteSettings = reqwest::get(format!("{}/settings", server.url()))
.await
.unwrap()
.json()
.await
.unwrap();
assert_eq!(settings.tips, Some(vec!["tip_v2".into()]));
assert_eq!(settings.leader_mode, Some(true));
})
.await;
}
/// Verify `fetch_settings_blocking` round-trips through the mock server.
/// This is the actual client function used by `refresh_remote_settings`.
#[tokio::test]
async fn test_fetch_settings_blocking_round_trip() {
with_local_set(|| async {
let server = MockInferenceServer::start()
.await
.expect("start mock server");
// Without settings configured: returns None (404 from mock)
let auth = kigi_shell::auth::KimiAuth {
key: "test-key".into(),
..Default::default()
};
let result = tokio::task::spawn_blocking({
let url = server.url().to_string();
let auth = auth.clone();
move || kigi_shell::remote::fetch_settings_blocking(&url, &auth, None)
})
.await
.unwrap();
assert!(
result.is_none(),
"Expected None when settings not configured"
);
// With settings configured: returns Some(settings)
server.set_settings(RemoteSettings {
tips: Some(vec!["fetched_tip".into()]),
..Default::default()
});
let result = tokio::task::spawn_blocking({
let url = server.url().to_string();
let auth = auth.clone();
move || kigi_shell::remote::fetch_settings_blocking(&url, &auth, None)
})
.await
.unwrap();
let settings = result.expect("Expected Some when settings are configured");
assert_eq!(settings.tips, Some(vec!["fetched_tip".into()]));
})
.await;
}
/// Verify the `doom_loop_recovery` settings object survives the
/// `/v1/settings` round-trip, that its absence deserializes to `None` (old
/// servers), and that a partial object keeps its unset fields `None`.
#[tokio::test]
async fn test_doom_loop_recovery_settings_round_trip() {
use kigi_shell::util::config::DoomLoopRecoverySettings;
with_local_set(|| async {
let server = MockInferenceServer::start()
.await
.expect("start mock server");
// Absent from the payload ⇒ None on the client.
server.set_settings(RemoteSettings::default());
let settings: RemoteSettings = reqwest::get(format!("{}/settings", server.url()))
.await
.unwrap()
.json()
.await
.unwrap();
assert_eq!(settings.doom_loop_recovery, None);
server.set_settings(RemoteSettings {
doom_loop_recovery: Some(DoomLoopRecoverySettings {
enabled: Some(true),
max_threshold: Some(16),
max_retries: Some(1),
}),
..Default::default()
});
let settings: RemoteSettings = reqwest::get(format!("{}/settings", server.url()))
.await
.unwrap()
.json()
.await
.unwrap();
let recovery = settings.doom_loop_recovery.expect("object round-trips");
assert_eq!(recovery.enabled, Some(true));
assert_eq!(recovery.max_threshold, Some(16));
assert_eq!(recovery.max_retries, Some(1));
// Partial object: only the set field comes through; the rest stay
// None so the resolver falls through per-field.
server.set_settings(RemoteSettings {
doom_loop_recovery: Some(DoomLoopRecoverySettings {
max_threshold: Some(32),
..Default::default()
}),
..Default::default()
});
let settings: RemoteSettings = reqwest::get(format!("{}/settings", server.url()))
.await
.unwrap()
.json()
.await
.unwrap();
let recovery = settings.doom_loop_recovery.expect("object round-trips");
assert_eq!(recovery.enabled, None);
assert_eq!(recovery.max_threshold, Some(32));
assert_eq!(recovery.max_retries, None);
})
.await;
}
/// Verify that the mock server's request log correctly tracks
/// GET /v1/settings requests for assertion in multi-session tests.
#[tokio::test]
async fn test_settings_requests_appear_in_request_log() {
with_local_set(|| async {
let server = MockInferenceServer::start()
.await
.expect("start mock server");
server.set_settings(RemoteSettings::default());
assert_eq!(server.request_count(), 0);
// First request
let _ = reqwest::get(format!("{}/settings", server.url()))
.await
.unwrap();
let settings_reqs: Vec<_> = server
.requests()
.into_iter()
.filter(|r| r.method == "GET" && r.path.contains("/settings"))
.collect();
assert_eq!(settings_reqs.len(), 1, "Expected 1 settings request");
// Second request (simulating /new refresh)
let _ = reqwest::get(format!("{}/settings", server.url()))
.await
.unwrap();
let settings_reqs: Vec<_> = server
.requests()
.into_iter()
.filter(|r| r.method == "GET" && r.path.contains("/settings"))
.collect();
assert_eq!(settings_reqs.len(), 2, "Expected 2 settings requests");
})
.await;
}