mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
feat(provider): 原生接入 Windsurf provider
This commit is contained in:
@@ -922,6 +922,317 @@ pub fn parse_kiro_usage_response(
|
||||
Some(serde_json::Value::Object(result))
|
||||
}
|
||||
|
||||
pub fn parse_windsurf_user_status_response(
|
||||
value: &serde_json::Value,
|
||||
updated_at_unix_secs: u64,
|
||||
) -> Option<serde_json::Value> {
|
||||
let user_status = value
|
||||
.get("userStatus")
|
||||
.or_else(|| value.get("user_status"))?;
|
||||
let plan_status = user_status
|
||||
.get("planStatus")
|
||||
.or_else(|| user_status.get("plan_status"))?;
|
||||
let plan_info = plan_status
|
||||
.get("planInfo")
|
||||
.or_else(|| plan_status.get("plan_info"));
|
||||
|
||||
let mut result = serde_json::Map::new();
|
||||
result.insert("updated_at".to_string(), json!(updated_at_unix_secs));
|
||||
|
||||
if let Some(plan_name) = plan_info
|
||||
.and_then(|value| {
|
||||
coerce_json_string(value.get("planName").or_else(|| value.get("plan_name")))
|
||||
})
|
||||
.or_else(|| {
|
||||
coerce_json_string(
|
||||
plan_status
|
||||
.get("planName")
|
||||
.or_else(|| plan_status.get("plan_name")),
|
||||
)
|
||||
})
|
||||
{
|
||||
result.insert("plan_name".to_string(), json!(plan_name));
|
||||
}
|
||||
if let Some(email) = coerce_json_string(user_status.get("email")) {
|
||||
result.insert("email".to_string(), json!(email));
|
||||
}
|
||||
if let Some(value) = plan_status
|
||||
.get("dailyQuotaRemainingPercent")
|
||||
.or_else(|| plan_status.get("daily_quota_remaining_percent"))
|
||||
.and_then(coerce_json_f64)
|
||||
{
|
||||
result.insert("daily_remaining_percent".to_string(), json!(value));
|
||||
}
|
||||
if let Some(value) = plan_status
|
||||
.get("weeklyQuotaRemainingPercent")
|
||||
.or_else(|| plan_status.get("weekly_quota_remaining_percent"))
|
||||
.and_then(coerce_json_f64)
|
||||
{
|
||||
result.insert("weekly_remaining_percent".to_string(), json!(value));
|
||||
}
|
||||
if let Some(value) = plan_status
|
||||
.get("dailyQuotaResetAtUnix")
|
||||
.or_else(|| plan_status.get("daily_quota_reset_at_unix"))
|
||||
.and_then(coerce_json_u64)
|
||||
{
|
||||
result.insert("daily_reset_at".to_string(), json!(value));
|
||||
}
|
||||
if let Some(value) = plan_status
|
||||
.get("weeklyQuotaResetAtUnix")
|
||||
.or_else(|| plan_status.get("weekly_quota_reset_at_unix"))
|
||||
.and_then(coerce_json_u64)
|
||||
{
|
||||
result.insert("weekly_reset_at".to_string(), json!(value));
|
||||
}
|
||||
if let Some(value) = plan_status
|
||||
.get("overageBalanceMicros")
|
||||
.or_else(|| plan_status.get("overage_balance_micros"))
|
||||
.and_then(coerce_json_f64)
|
||||
{
|
||||
result.insert("overage_balance".to_string(), json!(value / 1_000_000.0));
|
||||
}
|
||||
|
||||
let legacy_credit =
|
||||
|value: Option<&serde_json::Value>| value.and_then(coerce_json_f64).map(|n| n / 100.0);
|
||||
if let Some(value) = legacy_credit(
|
||||
plan_status
|
||||
.get("availablePromptCredits")
|
||||
.or_else(|| plan_status.get("available_prompt_credits")),
|
||||
) {
|
||||
result.insert("prompt_remaining".to_string(), json!(value));
|
||||
}
|
||||
if let Some(value) = legacy_credit(
|
||||
plan_status
|
||||
.get("usedPromptCredits")
|
||||
.or_else(|| plan_status.get("used_prompt_credits")),
|
||||
) {
|
||||
result.insert("prompt_used".to_string(), json!(value));
|
||||
}
|
||||
if let Some(value) = legacy_credit(plan_info.and_then(|plan_info| {
|
||||
plan_info
|
||||
.get("monthlyPromptCredits")
|
||||
.or_else(|| plan_info.get("monthly_prompt_credits"))
|
||||
})) {
|
||||
result.insert("prompt_limit".to_string(), json!(value));
|
||||
}
|
||||
if let Some(value) = legacy_credit(
|
||||
plan_status
|
||||
.get("availableFlexCredits")
|
||||
.or_else(|| plan_status.get("available_flex_credits")),
|
||||
) {
|
||||
result.insert("flex_remaining".to_string(), json!(value));
|
||||
}
|
||||
if let Some(value) = legacy_credit(
|
||||
plan_status
|
||||
.get("usedFlexCredits")
|
||||
.or_else(|| plan_status.get("used_flex_credits")),
|
||||
) {
|
||||
result.insert("flex_used".to_string(), json!(value));
|
||||
}
|
||||
if let Some(value) = legacy_credit(plan_info.and_then(|plan_info| {
|
||||
plan_info
|
||||
.get("monthlyFlexCreditPurchaseAmount")
|
||||
.or_else(|| plan_info.get("monthly_flex_credit_purchase_amount"))
|
||||
})) {
|
||||
result.insert("flex_limit".to_string(), json!(value));
|
||||
}
|
||||
|
||||
let mut status_sources = vec![value, user_status, plan_status];
|
||||
if let Some(plan_info) = plan_info {
|
||||
status_sources.push(plan_info);
|
||||
}
|
||||
for (target, aliases) in [
|
||||
(
|
||||
"banned",
|
||||
&[
|
||||
"banned",
|
||||
"isBanned",
|
||||
"is_banned",
|
||||
"accountBanned",
|
||||
"account_banned",
|
||||
][..],
|
||||
),
|
||||
(
|
||||
"quarantined",
|
||||
&[
|
||||
"quarantined",
|
||||
"isQuarantined",
|
||||
"is_quarantined",
|
||||
"accountQuarantined",
|
||||
"account_quarantined",
|
||||
][..],
|
||||
),
|
||||
(
|
||||
"is_forbidden",
|
||||
&[
|
||||
"isForbidden",
|
||||
"is_forbidden",
|
||||
"forbidden",
|
||||
"accountForbidden",
|
||||
"account_forbidden",
|
||||
][..],
|
||||
),
|
||||
] {
|
||||
if let Some(found) = status_sources.iter().find_map(|source| {
|
||||
aliases
|
||||
.iter()
|
||||
.find_map(|alias| source.get(*alias).and_then(coerce_json_bool))
|
||||
}) {
|
||||
result.insert(target.to_string(), json!(found));
|
||||
}
|
||||
}
|
||||
for (target, aliases) in [
|
||||
(
|
||||
"ban_reason",
|
||||
&[
|
||||
"banReason",
|
||||
"ban_reason",
|
||||
"blockedReason",
|
||||
"blocked_reason",
|
||||
"reason",
|
||||
"message",
|
||||
][..],
|
||||
),
|
||||
(
|
||||
"quarantine_reason",
|
||||
&["quarantineReason", "quarantine_reason", "reason", "message"][..],
|
||||
),
|
||||
(
|
||||
"forbidden_reason",
|
||||
&["forbiddenReason", "forbidden_reason", "reason", "message"][..],
|
||||
),
|
||||
] {
|
||||
if let Some(found) = status_sources.iter().find_map(|source| {
|
||||
aliases
|
||||
.iter()
|
||||
.find_map(|alias| coerce_json_string(source.get(*alias)))
|
||||
}) {
|
||||
result.insert(target.to_string(), json!(found));
|
||||
}
|
||||
}
|
||||
|
||||
Some(serde_json::Value::Object(result))
|
||||
}
|
||||
|
||||
pub fn parse_windsurf_model_configs_response(
|
||||
value: &serde_json::Value,
|
||||
updated_at_unix_secs: u64,
|
||||
) -> Option<serde_json::Value> {
|
||||
let configs = value
|
||||
.get("clientModelConfigs")
|
||||
.or_else(|| value.get("client_model_configs"))
|
||||
.and_then(serde_json::Value::as_array)?;
|
||||
let mut models = Vec::new();
|
||||
for config in configs {
|
||||
let Some(model_uid) = coerce_json_string(
|
||||
config
|
||||
.get("modelUid")
|
||||
.or_else(|| config.get("model_uid"))
|
||||
.or_else(|| config.get("id"))
|
||||
.or_else(|| config.get("name")),
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
let mut model = serde_json::Map::new();
|
||||
model.insert("model_uid".to_string(), json!(model_uid));
|
||||
if let Some(label) = coerce_json_string(
|
||||
config
|
||||
.get("label")
|
||||
.or_else(|| config.get("displayName"))
|
||||
.or_else(|| config.get("display_name")),
|
||||
) {
|
||||
model.insert("label".to_string(), json!(label));
|
||||
}
|
||||
if let Some(provider) = coerce_json_string(config.get("provider")) {
|
||||
model.insert("provider".to_string(), json!(provider));
|
||||
}
|
||||
if let Some(value) = config
|
||||
.get("supportsImages")
|
||||
.or_else(|| config.get("supports_images"))
|
||||
.and_then(coerce_json_bool)
|
||||
{
|
||||
model.insert("supports_images".to_string(), json!(value));
|
||||
}
|
||||
if let Some(value) = config
|
||||
.get("creditMultiplier")
|
||||
.or_else(|| config.get("credit_multiplier"))
|
||||
.and_then(coerce_json_f64)
|
||||
{
|
||||
model.insert("credit_multiplier".to_string(), json!(value));
|
||||
}
|
||||
models.push(serde_json::Value::Object(model));
|
||||
}
|
||||
|
||||
let mut result = serde_json::Map::new();
|
||||
result.insert("updated_at".to_string(), json!(updated_at_unix_secs));
|
||||
result.insert(
|
||||
"allowed_models_count".to_string(),
|
||||
json!(models.len() as u64),
|
||||
);
|
||||
result.insert("models".to_string(), serde_json::Value::Array(models));
|
||||
if let Some(default_model_uid) = value
|
||||
.get("defaultOverrideModelConfig")
|
||||
.or_else(|| value.get("default_override_model_config"))
|
||||
.and_then(|default_config| {
|
||||
coerce_json_string(
|
||||
default_config
|
||||
.get("modelUid")
|
||||
.or_else(|| default_config.get("model_uid")),
|
||||
)
|
||||
})
|
||||
{
|
||||
result.insert("default_model_uid".to_string(), json!(default_model_uid));
|
||||
}
|
||||
|
||||
Some(serde_json::Value::Object(result))
|
||||
}
|
||||
|
||||
pub fn parse_windsurf_rate_limit_response(
|
||||
value: &serde_json::Value,
|
||||
updated_at_unix_secs: u64,
|
||||
) -> Option<serde_json::Value> {
|
||||
let root = value.as_object()?;
|
||||
if root.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let has_capacity = value
|
||||
.get("hasCapacity")
|
||||
.or_else(|| value.get("has_capacity"))
|
||||
.and_then(coerce_json_bool)
|
||||
.unwrap_or(true);
|
||||
let messages_remaining = value
|
||||
.get("messagesRemaining")
|
||||
.or_else(|| value.get("messages_remaining"))
|
||||
.and_then(coerce_json_f64);
|
||||
let max_messages = value
|
||||
.get("maxMessages")
|
||||
.or_else(|| value.get("max_messages"))
|
||||
.and_then(coerce_json_f64);
|
||||
let retry_after_ms = value
|
||||
.get("retryAfterMs")
|
||||
.or_else(|| value.get("retry_after_ms"))
|
||||
.and_then(coerce_json_u64);
|
||||
|
||||
let limited = !has_capacity || messages_remaining.is_some_and(|value| value <= 0.0);
|
||||
let mut rate_limit = serde_json::Map::new();
|
||||
rate_limit.insert("limited".to_string(), json!(limited));
|
||||
rate_limit.insert("has_capacity".to_string(), json!(has_capacity));
|
||||
if let Some(value) = messages_remaining {
|
||||
rate_limit.insert("messages_remaining".to_string(), json!(value));
|
||||
}
|
||||
if let Some(value) = max_messages {
|
||||
rate_limit.insert("max_messages".to_string(), json!(value));
|
||||
}
|
||||
if let Some(value) = retry_after_ms {
|
||||
rate_limit.insert("retry_after_ms".to_string(), json!(value));
|
||||
}
|
||||
|
||||
Some(json!({
|
||||
"updated_at": updated_at_unix_secs,
|
||||
"rate_limit": rate_limit,
|
||||
}))
|
||||
}
|
||||
|
||||
fn chatgpt_web_quota_feature_name(value: &serde_json::Value) -> Option<String> {
|
||||
coerce_json_string(
|
||||
value
|
||||
@@ -1141,8 +1452,10 @@ mod tests {
|
||||
use super::{
|
||||
codex_build_invalid_state, codex_runtime_invalid_reason,
|
||||
parse_chatgpt_web_conversation_init_response, parse_codex_backend_me_response,
|
||||
parse_codex_wham_usage_response, OAUTH_ACCOUNT_BLOCK_PREFIX, OAUTH_EXPIRED_PREFIX,
|
||||
OAUTH_REFRESH_FAILED_PREFIX, OAUTH_REQUEST_FAILED_PREFIX,
|
||||
parse_codex_wham_usage_response, parse_windsurf_model_configs_response,
|
||||
parse_windsurf_rate_limit_response, parse_windsurf_user_status_response,
|
||||
OAUTH_ACCOUNT_BLOCK_PREFIX, OAUTH_EXPIRED_PREFIX, OAUTH_REFRESH_FAILED_PREFIX,
|
||||
OAUTH_REQUEST_FAILED_PREFIX,
|
||||
};
|
||||
use aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey;
|
||||
use serde_json::json;
|
||||
@@ -1504,6 +1817,118 @@ mod tests {
|
||||
assert!(parsed.get("secondary_used_percent").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_windsurf_user_status_response() {
|
||||
let parsed = parse_windsurf_user_status_response(
|
||||
&json!({
|
||||
"userStatus": {
|
||||
"email": "windsurf@example.com",
|
||||
"isQuarantined": true,
|
||||
"quarantineReason": "quota review",
|
||||
"planStatus": {
|
||||
"dailyQuotaRemainingPercent": 45.5,
|
||||
"weeklyQuotaRemainingPercent": 80,
|
||||
"dailyQuotaResetAtUnix": "1775553285",
|
||||
"weeklyQuotaResetAtUnix": 1776158085u64,
|
||||
"availablePromptCredits": 900,
|
||||
"usedPromptCredits": 100,
|
||||
"availableFlexCredits": 250,
|
||||
"usedFlexCredits": 50,
|
||||
"overageBalanceMicros": 1250000,
|
||||
"planInfo": {
|
||||
"planName": "Pro",
|
||||
"monthlyPromptCredits": 1000,
|
||||
"monthlyFlexCreditPurchaseAmount": 300
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
1_770_000_000,
|
||||
)
|
||||
.expect("windsurf user status should parse");
|
||||
|
||||
assert_eq!(parsed.get("plan_name"), Some(&json!("Pro")));
|
||||
assert_eq!(parsed.get("daily_remaining_percent"), Some(&json!(45.5)));
|
||||
assert_eq!(parsed.get("weekly_remaining_percent"), Some(&json!(80.0)));
|
||||
assert_eq!(parsed.get("daily_reset_at"), Some(&json!(1_775_553_285u64)));
|
||||
assert_eq!(
|
||||
parsed.get("weekly_reset_at"),
|
||||
Some(&json!(1_776_158_085u64))
|
||||
);
|
||||
assert_eq!(parsed.get("prompt_remaining"), Some(&json!(9.0)));
|
||||
assert_eq!(parsed.get("prompt_used"), Some(&json!(1.0)));
|
||||
assert_eq!(parsed.get("prompt_limit"), Some(&json!(10.0)));
|
||||
assert_eq!(parsed.get("flex_remaining"), Some(&json!(2.5)));
|
||||
assert_eq!(parsed.get("flex_used"), Some(&json!(0.5)));
|
||||
assert_eq!(parsed.get("flex_limit"), Some(&json!(3.0)));
|
||||
assert_eq!(parsed.get("overage_balance"), Some(&json!(1.25)));
|
||||
assert_eq!(parsed.get("email"), Some(&json!("windsurf@example.com")));
|
||||
assert_eq!(parsed.get("quarantined"), Some(&json!(true)));
|
||||
assert_eq!(
|
||||
parsed.get("quarantine_reason"),
|
||||
Some(&json!("quota review"))
|
||||
);
|
||||
assert_eq!(parsed.get("updated_at"), Some(&json!(1_770_000_000u64)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_windsurf_model_configs_response() {
|
||||
let parsed = parse_windsurf_model_configs_response(
|
||||
&json!({
|
||||
"clientModelConfigs": [
|
||||
{
|
||||
"modelUid": "claude-sonnet-4-5",
|
||||
"label": "Claude Sonnet 4.5",
|
||||
"provider": "anthropic",
|
||||
"supportsImages": true,
|
||||
"creditMultiplier": 2
|
||||
},
|
||||
{
|
||||
"modelUid": "gpt-5-mini",
|
||||
"label": "GPT-5 mini"
|
||||
}
|
||||
],
|
||||
"defaultOverrideModelConfig": {
|
||||
"modelUid": "claude-sonnet-4-5"
|
||||
}
|
||||
}),
|
||||
1_770_000_100,
|
||||
)
|
||||
.expect("windsurf model configs should parse");
|
||||
|
||||
assert_eq!(parsed.get("allowed_models_count"), Some(&json!(2u64)));
|
||||
assert_eq!(
|
||||
parsed.get("default_model_uid"),
|
||||
Some(&json!("claude-sonnet-4-5"))
|
||||
);
|
||||
assert_eq!(parsed.get("updated_at"), Some(&json!(1_770_000_100u64)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_windsurf_rate_limit_response() {
|
||||
let parsed = parse_windsurf_rate_limit_response(
|
||||
&json!({
|
||||
"hasCapacity": false,
|
||||
"messagesRemaining": 0,
|
||||
"maxMessages": 25,
|
||||
"retryAfterMs": 45000
|
||||
}),
|
||||
1_770_000_200,
|
||||
)
|
||||
.expect("windsurf rate limit should parse");
|
||||
|
||||
assert_eq!(parsed.get("updated_at"), Some(&json!(1_770_000_200u64)));
|
||||
assert_eq!(parsed.pointer("/rate_limit/limited"), Some(&json!(true)));
|
||||
assert_eq!(
|
||||
parsed.pointer("/rate_limit/messages_remaining"),
|
||||
Some(&json!(0.0))
|
||||
);
|
||||
assert_eq!(
|
||||
parsed.pointer("/rate_limit/retry_after_ms"),
|
||||
Some(&json!(45000u64))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_chatgpt_web_image_quota_from_conversation_init() {
|
||||
let parsed = parse_chatgpt_web_conversation_init_response(
|
||||
|
||||
@@ -37,9 +37,27 @@ pub fn provider_oauth_pkce_s256(verifier: &str) -> String {
|
||||
|
||||
pub fn parse_provider_oauth_callback_params(callback_url: &str) -> BTreeMap<String, String> {
|
||||
let mut merged = BTreeMap::new();
|
||||
let Ok(url) = Url::parse(callback_url.trim()) else {
|
||||
let raw_callback_url = callback_url.trim();
|
||||
let parsed_url = Url::parse(raw_callback_url).or_else(|_| {
|
||||
Url::parse(&format!(
|
||||
"https://aether.local/{}",
|
||||
raw_callback_url.trim_start_matches('/')
|
||||
))
|
||||
});
|
||||
let Ok(url) = parsed_url else {
|
||||
return merged;
|
||||
};
|
||||
if url.query().is_none()
|
||||
&& url.fragment().is_none()
|
||||
&& raw_callback_url.contains('=')
|
||||
&& !raw_callback_url.contains("://")
|
||||
{
|
||||
for (key, value) in
|
||||
form_urlencoded::parse(raw_callback_url.trim_start_matches('?').as_bytes())
|
||||
{
|
||||
merged.insert(key.into_owned(), value.into_owned());
|
||||
}
|
||||
}
|
||||
for (key, value) in form_urlencoded::parse(url.query().unwrap_or_default().as_bytes()) {
|
||||
merged.insert(key.into_owned(), value.into_owned());
|
||||
}
|
||||
@@ -377,6 +395,28 @@ mod tests {
|
||||
assert_eq!(params.get("state").map(String::as_str), Some("nonce-value"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_provider_oauth_callback_params_reads_relative_show_auth_token_url() {
|
||||
let params = parse_provider_oauth_callback_params(
|
||||
"show-auth-token?token=firebase-id-token&state=session-1&provider=google",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
params.get("token").map(String::as_str),
|
||||
Some("firebase-id-token")
|
||||
);
|
||||
assert_eq!(params.get("state").map(String::as_str), Some("session-1"));
|
||||
assert_eq!(params.get("provider").map(String::as_str), Some("google"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_provider_oauth_callback_params_reads_raw_query_string() {
|
||||
let params = parse_provider_oauth_callback_params("token=raw-token&state=session-raw");
|
||||
|
||||
assert_eq!(params.get("token").map(String::as_str), Some("raw-token"));
|
||||
assert_eq!(params.get("state").map(String::as_str), Some("session-raw"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chatgpt_web_enrichment_extracts_identity_from_openai_claims() {
|
||||
let access_token = sample_unsigned_jwt(json!({
|
||||
|
||||
@@ -40,6 +40,7 @@ const AUTO_REMOVABLE_ACCOUNT_STATE_CODES: &[&str] = &[
|
||||
"account_banned",
|
||||
"account_suspended",
|
||||
"account_disabled",
|
||||
"account_quarantined",
|
||||
"workspace_deactivated",
|
||||
"account_forbidden",
|
||||
];
|
||||
@@ -275,7 +276,7 @@ fn resolve_from_metadata(
|
||||
upstream_metadata: Option<&Value>,
|
||||
) -> Option<PoolAccountState> {
|
||||
for source in metadata_sources(provider_type, upstream_metadata) {
|
||||
if json_bool(source.get("is_banned")) {
|
||||
if json_bool(source.get("is_banned")) || json_bool(source.get("banned")) {
|
||||
let reason = extract_reason(
|
||||
source,
|
||||
&["ban_reason", "forbidden_reason", "reason", "message"],
|
||||
@@ -290,6 +291,18 @@ fn resolve_from_metadata(
|
||||
recoverable: false,
|
||||
});
|
||||
}
|
||||
if json_bool(source.get("is_quarantined")) || json_bool(source.get("quarantined")) {
|
||||
let reason = extract_reason(source, &["quarantine_reason", "reason", "message"])
|
||||
.unwrap_or_else(|| "账号处于隔离状态".to_string());
|
||||
return Some(PoolAccountState {
|
||||
blocked: true,
|
||||
code: Some("account_quarantined".to_string()),
|
||||
label: Some("账号隔离".to_string()),
|
||||
reason: Some(reason),
|
||||
source: Some("metadata".to_string()),
|
||||
recoverable: false,
|
||||
});
|
||||
}
|
||||
if json_bool(source.get("is_forbidden")) || json_bool(source.get("account_disabled")) {
|
||||
let reason = extract_reason(
|
||||
source,
|
||||
@@ -574,6 +587,35 @@ mod tests {
|
||||
assert!(!should_auto_remove_account_state(&state));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_windsurf_banned_and_quarantined_metadata_aliases() {
|
||||
let banned = resolve_pool_account_state(
|
||||
Some("windsurf"),
|
||||
Some(&json!({
|
||||
"windsurf": {
|
||||
"banned": true,
|
||||
"reason": "forbidden"
|
||||
}
|
||||
})),
|
||||
None,
|
||||
);
|
||||
assert!(banned.blocked);
|
||||
assert_eq!(banned.code.as_deref(), Some("account_banned"));
|
||||
|
||||
let quarantined = resolve_pool_account_state(
|
||||
Some("windsurf"),
|
||||
Some(&json!({
|
||||
"windsurf": {
|
||||
"quarantined": true
|
||||
}
|
||||
})),
|
||||
None,
|
||||
);
|
||||
assert!(quarantined.blocked);
|
||||
assert_eq!(quarantined.code.as_deref(), Some("account_quarantined"));
|
||||
assert!(should_auto_remove_account_state(&quarantined));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_snapshot_ignores_refresh_failed_as_account_block() {
|
||||
let snapshot = resolve_account_status_snapshot(
|
||||
|
||||
Reference in New Issue
Block a user