use std::sync::Arc;
use rmcp::ErrorData;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use tokio::sync::Mutex;
use zendriver::UserAgentOverride;
use crate::errors::{McpServerError, map_error};
use crate::state::{SessionState, StealthOverrides, StealthProfileChoice};
use crate::tools::actions::AckOutput;
use crate::tools::common::current_tab;
#[derive(Debug, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct SetStealthProfileInput {
pub profile: StealthProfileChoice,
#[serde(default)]
pub overrides: StealthOverrides,
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct SetStealthProfileOutput {
pub active_profile: StealthProfileChoice,
pub active_overrides: StealthOverrides,
pub takes_effect_on_next_open: bool,
}
pub async fn set_stealth_profile(
state: Arc<Mutex<SessionState>>,
input: SetStealthProfileInput,
) -> Result<SetStealthProfileOutput, ErrorData> {
let mut s = state.lock().await;
s.stealth_profile_choice = input.profile;
s.stealth_overrides = input.overrides.clone();
Ok(SetStealthProfileOutput {
active_profile: input.profile,
active_overrides: input.overrides,
takes_effect_on_next_open: s.browser.is_some(),
})
}
#[derive(Debug, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct SetUserAgentInput {
pub user_agent: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub accept_language: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub platform: Option<String>,
}
pub async fn set_user_agent(
state: Arc<Mutex<SessionState>>,
input: SetUserAgentInput,
) -> Result<AckOutput, ErrorData> {
let s = state.lock().await;
let tab = current_tab(&s).await?;
tab.set_user_agent_with(UserAgentOverride {
user_agent: input.user_agent,
accept_language: input.accept_language,
platform: input.platform,
})
.await
.map_err(|e| map_error(McpServerError::from(e)))?;
Ok(AckOutput { ok: true })
}
#[cfg(test)]
mod tests {
use super::*;
fn fresh() -> Arc<Mutex<SessionState>> {
Arc::new(Mutex::new(SessionState::new()))
}
#[tokio::test]
async fn set_profile_without_browser_reports_immediate_effect() {
let state = fresh();
let out = set_stealth_profile(
state.clone(),
SetStealthProfileInput {
profile: StealthProfileChoice::SpoofLinux,
overrides: StealthOverrides::default(),
},
)
.await
.expect("set profile ok");
assert_eq!(out.active_profile, StealthProfileChoice::SpoofLinux);
assert!(
!out.takes_effect_on_next_open,
"no browser open, so the next open call picks the new profile up directly"
);
let s = state.lock().await;
assert_eq!(s.stealth_profile_choice, StealthProfileChoice::SpoofLinux);
}
#[tokio::test]
async fn set_profile_overwrites_previous_choice() {
let state = fresh();
set_stealth_profile(
state.clone(),
SetStealthProfileInput {
profile: StealthProfileChoice::SpoofMacos,
overrides: StealthOverrides::default(),
},
)
.await
.expect("first set ok");
let out = set_stealth_profile(
state.clone(),
SetStealthProfileInput {
profile: StealthProfileChoice::SpoofWindows,
overrides: StealthOverrides::default(),
},
)
.await
.expect("second set ok");
assert_eq!(out.active_profile, StealthProfileChoice::SpoofWindows);
let s = state.lock().await;
assert_eq!(s.stealth_profile_choice, StealthProfileChoice::SpoofWindows);
}
#[tokio::test]
async fn set_profile_does_not_fail_when_choice_matches_existing() {
let state = fresh();
let out = set_stealth_profile(
state,
SetStealthProfileInput {
profile: StealthProfileChoice::Auto,
overrides: StealthOverrides::default(),
},
)
.await
.expect("idempotent set ok");
assert_eq!(out.active_profile, StealthProfileChoice::Auto);
assert!(!out.takes_effect_on_next_open);
}
}