mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-01 12:22:09 +08:00
feat(grokbuild): add Grok Official provider with official-state import
Add a "Grok Official" preset and seed (grokbuild-official) whose empty config represents the official login state: no custom [model.*] tables are written, so Grok CLI falls back to its built-in xAI OAuth login and cc-switch never touches those credentials. Backend: - Seed entry in OFFICIAL_SEEDS plus ensure_grokbuild_official_provider command for on-demand repair (the one-shot master seeding flag is already set for existing databases). - Split validation into syntax-only (empty allowed) for live reads, writes and official snapshots, keeping the full custom-model shape check for non-official provider writes and imports. Backup/restore can now round-trip an official-state live file. - Manual import (command layer only) recognizes an official-state live config and imports it as the official entry set as current, matching the Codex official-login import outcome. Startup auto-import keeps rejecting official-state live so a deleted official entry is never resurrected on launch: startup import only captures real user data as "default" and never manufactures official entries. - Manual import also ensures the official entry before importing (claude-desktop precedent) and after a successful custom import, so first-time users end up with default + official like other apps. - Proxy takeover guards skip or reject official-state live configs in all three takeover paths, consistent with the official-provider takeover ban. Frontend: - Grok Official preset entry in the GrokBuild form: official category hides connection fields and passes the raw config through untouched. - Filter managed-OAuth presets out of the GrokBuild preset list; they were never wired for this app and produced keyless broken configs. Tests cover seed presence, official round-trip, ensure-after-deletion, and the four import scenarios including startup non-resurrection.
This commit is contained in:
@@ -4,9 +4,9 @@ use std::fs;
|
||||
use serde_json::json;
|
||||
|
||||
use cc_switch_lib::{
|
||||
get_claude_mcp_path, get_claude_mcp_status, get_claude_settings_path,
|
||||
get_claude_mcp_path, get_claude_mcp_status, get_claude_settings_path, get_grok_config_path,
|
||||
import_default_config_test_hook, read_claude_mcp_config, update_settings, AppError,
|
||||
AppSettings, AppType, McpApps, McpServer, McpService, MultiAppConfig,
|
||||
AppSettings, AppType, McpApps, McpServer, McpService, MultiAppConfig, ProviderService,
|
||||
};
|
||||
|
||||
#[path = "support.rs"]
|
||||
@@ -68,6 +68,185 @@ fn import_default_config_claude_persists_provider() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_default_config_grokbuild_seeds_official_alongside_default() {
|
||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||
reset_test_fs();
|
||||
let _home = ensure_test_home();
|
||||
|
||||
let config_path = get_grok_config_path();
|
||||
if let Some(parent) = config_path.parent() {
|
||||
fs::create_dir_all(parent).expect("create grok config dir");
|
||||
}
|
||||
fs::write(
|
||||
&config_path,
|
||||
r#"[models]
|
||||
default = "grok-4.5"
|
||||
|
||||
[model."grok-4.5"]
|
||||
model = "grok-4.5"
|
||||
base_url = "https://example.com/v1"
|
||||
name = "Example"
|
||||
api_key = "secret"
|
||||
api_backend = "responses"
|
||||
context_window = 500000
|
||||
"#,
|
||||
)
|
||||
.expect("seed grok config.toml");
|
||||
|
||||
let mut config = MultiAppConfig::default();
|
||||
config.ensure_app(&AppType::GrokBuild);
|
||||
let state = create_test_state_with_config(&config).expect("create test state");
|
||||
|
||||
import_default_config_test_hook(&state, AppType::GrokBuild)
|
||||
.expect("import default config succeeds");
|
||||
|
||||
let providers = state
|
||||
.db
|
||||
.get_all_providers(AppType::GrokBuild.as_str())
|
||||
.expect("get all providers");
|
||||
assert!(
|
||||
providers.get("default").is_some(),
|
||||
"live imported as default"
|
||||
);
|
||||
|
||||
// 初次导入已有配置时应同时补出官方入口(其它应用靠首启动主播种,
|
||||
// grokbuild 种子晚于该 flag,挂在导入动作上)
|
||||
let official = providers
|
||||
.get("grokbuild-official")
|
||||
.expect("official seed ensured alongside import");
|
||||
assert_eq!(official.category.as_deref(), Some("official"));
|
||||
|
||||
// 激活的仍是导入的原配置,官方入口只是备选
|
||||
let current_id = state
|
||||
.db
|
||||
.get_current_provider(AppType::GrokBuild.as_str())
|
||||
.expect("get current provider");
|
||||
assert_eq!(current_id.as_deref(), Some("default"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_default_config_grokbuild_official_live_imports_official_as_current() {
|
||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||
reset_test_fs();
|
||||
let _home = ensure_test_home();
|
||||
|
||||
// 官方登录态的 live:无自定义模型表(允许 MCP 等其它内容)。
|
||||
// 导入的正确结果 = Grok Official 成为当前供应商,而非报错。
|
||||
let config_path = get_grok_config_path();
|
||||
if let Some(parent) = config_path.parent() {
|
||||
fs::create_dir_all(parent).expect("create grok config dir");
|
||||
}
|
||||
fs::write(&config_path, "[mcp_servers.echo]\ncommand = \"echo\"\n")
|
||||
.expect("seed official-mode grok config.toml");
|
||||
|
||||
let mut config = MultiAppConfig::default();
|
||||
config.ensure_app(&AppType::GrokBuild);
|
||||
let state = create_test_state_with_config(&config).expect("create test state");
|
||||
|
||||
let imported = import_default_config_test_hook(&state, AppType::GrokBuild)
|
||||
.expect("official-mode live imports as the official provider");
|
||||
assert!(imported, "official-mode import should report success");
|
||||
|
||||
let providers = state
|
||||
.db
|
||||
.get_all_providers(AppType::GrokBuild.as_str())
|
||||
.expect("get all providers");
|
||||
let official = providers
|
||||
.get("grokbuild-official")
|
||||
.expect("official entry ensured by import");
|
||||
assert_eq!(official.category.as_deref(), Some("official"));
|
||||
assert!(
|
||||
providers.get("default").is_none(),
|
||||
"official-mode live must not be imported as a custom default"
|
||||
);
|
||||
|
||||
let current_id = state
|
||||
.db
|
||||
.get_current_provider(AppType::GrokBuild.as_str())
|
||||
.expect("get current provider");
|
||||
assert_eq!(
|
||||
current_id.as_deref(),
|
||||
Some("grokbuild-official"),
|
||||
"official entry should become current to mirror the live state"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_import_grokbuild_official_live_does_not_resurrect_official() {
|
||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||
reset_test_fs();
|
||||
let _home = ensure_test_home();
|
||||
|
||||
// 启动自动导入走 service 层(lib.rs 启动循环直接调用它):官方态 live
|
||||
// 必须报错且不产出任何条目——全项目惯例是启动自动导入只产出 default、
|
||||
// 从不产出官方条目,否则删掉的官方条目每次重启都会复活。
|
||||
// 官方态的成功导入只挂在手动导入的命令层。
|
||||
let config_path = get_grok_config_path();
|
||||
if let Some(parent) = config_path.parent() {
|
||||
fs::create_dir_all(parent).expect("create grok config dir");
|
||||
}
|
||||
fs::write(&config_path, "").expect("seed empty official-mode grok config.toml");
|
||||
|
||||
let mut config = MultiAppConfig::default();
|
||||
config.ensure_app(&AppType::GrokBuild);
|
||||
let state = create_test_state_with_config(&config).expect("create test state");
|
||||
|
||||
ProviderService::import_default_config(&state, AppType::GrokBuild)
|
||||
.expect_err("startup auto-import must not import official-mode live");
|
||||
|
||||
let providers = state
|
||||
.db
|
||||
.get_all_providers(AppType::GrokBuild.as_str())
|
||||
.expect("get all providers");
|
||||
assert!(
|
||||
providers.is_empty(),
|
||||
"startup auto-import must not create any provider from official-mode live"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_default_config_grokbuild_broken_custom_live_still_errors() {
|
||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||
reset_test_fs();
|
||||
let _home = ensure_test_home();
|
||||
|
||||
// 有自定义痕迹但残缺([models] 存在、缺 [model.*]):必须报真实错误,
|
||||
// 不能被误判成官方态静默吞掉;官方入口仍由命令层前置 ensure 补出。
|
||||
let config_path = get_grok_config_path();
|
||||
if let Some(parent) = config_path.parent() {
|
||||
fs::create_dir_all(parent).expect("create grok config dir");
|
||||
}
|
||||
fs::write(&config_path, "[models]\ndefault = \"grok-4.5\"\n")
|
||||
.expect("seed broken custom grok config.toml");
|
||||
|
||||
let mut config = MultiAppConfig::default();
|
||||
config.ensure_app(&AppType::GrokBuild);
|
||||
let state = create_test_state_with_config(&config).expect("create test state");
|
||||
|
||||
import_default_config_test_hook(&state, AppType::GrokBuild)
|
||||
.expect_err("broken custom config should surface a validation error");
|
||||
|
||||
let providers = state
|
||||
.db
|
||||
.get_all_providers(AppType::GrokBuild.as_str())
|
||||
.expect("get all providers");
|
||||
assert!(
|
||||
providers.get("grokbuild-official").is_some(),
|
||||
"official entry still appears via the pre-import ensure"
|
||||
);
|
||||
assert!(providers.get("default").is_none(), "nothing was imported");
|
||||
let current_id = state
|
||||
.db
|
||||
.get_current_provider(AppType::GrokBuild.as_str())
|
||||
.expect("get current provider");
|
||||
assert_ne!(
|
||||
current_id.as_deref(),
|
||||
Some("grokbuild-official"),
|
||||
"failed import must not silently activate the official entry"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_default_config_without_live_file_returns_error() {
|
||||
use support::create_test_state;
|
||||
|
||||
Reference in New Issue
Block a user