689 lines
25 KiB
Rust
689 lines
25 KiB
Rust
#[allow(dead_code)]
|
|
#[path = "acp_common_tests/mod.rs"]
|
|
mod common_tests;
|
|
|
|
use common_tests::fixtures::server::AcpServerConnection;
|
|
use common_tests::fixtures::{run_test, send_custom, Connection, TestConnectionConfig};
|
|
use goose::config::base::CONFIG_YAML_NAME;
|
|
use goose::config::declarative_providers::load_provider;
|
|
use goose::config::paths::Paths;
|
|
use goose::config::{Config, ConfigError, DeclarativeProviderConfig};
|
|
use goose_test_support::EnforceSessionId;
|
|
use serial_test::serial;
|
|
use std::sync::Arc;
|
|
|
|
fn write_config(config_dir: &std::path::Path, contents: &str) {
|
|
std::fs::create_dir_all(config_dir).unwrap();
|
|
std::fs::write(config_dir.join(CONFIG_YAML_NAME), contents).unwrap();
|
|
}
|
|
|
|
fn write_secrets(config_dir: &std::path::Path, contents: &str) {
|
|
std::fs::write(config_dir.join("secrets.yaml"), contents).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn acp_catalog_and_custom_provider_methods_use_core_provider_store() {
|
|
let root = tempfile::tempdir().unwrap();
|
|
let root_path = root.path().to_string_lossy().to_string();
|
|
let _env = env_lock::lock_env([
|
|
("GOOSE_PATH_ROOT", Some(root_path.as_str())),
|
|
("GOOSE_DISABLE_KEYRING", Some("1")),
|
|
("XAI_API_KEY", None),
|
|
("XAI_HOST", None),
|
|
("CUSTOM_STARK_ACP_PROVIDER_API_KEY", None),
|
|
]);
|
|
|
|
let config_dir = Paths::config_dir();
|
|
write_config(
|
|
&config_dir,
|
|
"GOOSE_MODEL: gpt-4o\nGOOSE_PROVIDER: openai\nGOOSE_DISABLE_KEYRING: true\nXAI_HOST: https://api.x.ai/v1\n",
|
|
);
|
|
write_secrets(&config_dir, "XAI_API_KEY: xai-configured-key\n");
|
|
Config::global().invalidate_secrets_cache();
|
|
|
|
run_test(async move {
|
|
let openai = common_tests::fixtures::OpenAiFixture::new(
|
|
vec![],
|
|
Arc::new(EnforceSessionId::default()),
|
|
)
|
|
.await;
|
|
let config = TestConnectionConfig {
|
|
data_root: config_dir.clone(),
|
|
..Default::default()
|
|
};
|
|
let conn = AcpServerConnection::new(config, openai).await;
|
|
|
|
let catalog = send_custom(
|
|
conn.cx(),
|
|
"_goose/unstable/providers/catalog/list",
|
|
serde_json::json!({ "format": "openai" }),
|
|
)
|
|
.await
|
|
.expect("provider catalog list should succeed");
|
|
let catalog_providers = catalog
|
|
.get("providers")
|
|
.and_then(|providers| providers.as_array())
|
|
.expect("catalog response should include providers");
|
|
assert!(
|
|
catalog_providers
|
|
.iter()
|
|
.any(|provider| provider.get("providerId") == Some(&serde_json::json!("opencode"))),
|
|
"OpenAI-compatible catalog should include OpenCode Zen"
|
|
);
|
|
|
|
let setup_catalog = send_custom(
|
|
conn.cx(),
|
|
"_goose/unstable/providers/setup/catalog/list",
|
|
serde_json::json!({}),
|
|
)
|
|
.await
|
|
.expect("provider setup catalog list should succeed");
|
|
let setup_providers = setup_catalog
|
|
.get("providers")
|
|
.and_then(|providers| providers.as_array())
|
|
.expect("setup catalog response should include providers");
|
|
for provider_id in [
|
|
"goose",
|
|
"anthropic",
|
|
"openai",
|
|
"claude-acp",
|
|
"codex-acp",
|
|
"copilot-acp",
|
|
"amp-acp",
|
|
"cursor-agent",
|
|
"pi-acp",
|
|
] {
|
|
assert!(
|
|
setup_providers
|
|
.iter()
|
|
.any(|provider| provider.get("providerId")
|
|
== Some(&serde_json::json!(provider_id))),
|
|
"setup catalog should include {provider_id}"
|
|
);
|
|
}
|
|
for provider_id in ["codex", "claude_code", "gemini_cli"] {
|
|
assert!(
|
|
setup_providers
|
|
.iter()
|
|
.all(|provider| provider.get("providerId")
|
|
!= Some(&serde_json::json!(provider_id))),
|
|
"setup catalog should exclude deprecated provider {provider_id}"
|
|
);
|
|
}
|
|
let codex_setup = setup_providers
|
|
.iter()
|
|
.find(|provider| provider.get("providerId") == Some(&serde_json::json!("codex-acp")))
|
|
.expect("setup catalog should include codex-acp");
|
|
assert_eq!(
|
|
codex_setup.get("category"),
|
|
Some(&serde_json::json!("agent"))
|
|
);
|
|
assert_eq!(
|
|
codex_setup.get("setupMethod"),
|
|
Some(&serde_json::json!("cli_auth"))
|
|
);
|
|
assert_eq!(
|
|
codex_setup.get("supportsInstall"),
|
|
Some(&serde_json::json!(true))
|
|
);
|
|
assert!(
|
|
codex_setup
|
|
.get("aliases")
|
|
.and_then(|aliases| aliases.as_array())
|
|
.is_some_and(|aliases| aliases.contains(&serde_json::json!("codex"))),
|
|
"codex-acp setup aliases should include codex"
|
|
);
|
|
|
|
let template = send_custom(
|
|
conn.cx(),
|
|
"_goose/unstable/providers/catalog/template",
|
|
serde_json::json!({ "providerId": "zai" }),
|
|
)
|
|
.await
|
|
.expect("provider catalog template should succeed");
|
|
assert_eq!(
|
|
template.pointer("/template/providerId"),
|
|
Some(&serde_json::json!("zai"))
|
|
);
|
|
assert!(
|
|
template
|
|
.pointer("/template/models")
|
|
.and_then(|models| models.as_array())
|
|
.is_some_and(|models| !models.is_empty()),
|
|
"provider template should expose model templates"
|
|
);
|
|
|
|
let configured_status = send_custom(
|
|
conn.cx(),
|
|
"_goose/unstable/providers/config/status",
|
|
serde_json::json!({ "providerIds": ["xai"] }),
|
|
)
|
|
.await
|
|
.expect("provider config status should succeed");
|
|
assert_eq!(
|
|
configured_status.pointer("/statuses/0"),
|
|
Some(&serde_json::json!({
|
|
"providerId": "xai",
|
|
"isConfigured": true,
|
|
})),
|
|
"provider configured through core config should be configured through ACP"
|
|
);
|
|
|
|
let configured_read = send_custom(
|
|
conn.cx(),
|
|
"_goose/unstable/providers/config/read",
|
|
serde_json::json!({ "providerId": "xai" }),
|
|
)
|
|
.await
|
|
.expect("provider config read should succeed");
|
|
let fields = configured_read
|
|
.get("fields")
|
|
.and_then(|fields| fields.as_array())
|
|
.expect("provider config read should include fields");
|
|
let xai_key = fields
|
|
.iter()
|
|
.find(|field| field.get("key") == Some(&serde_json::json!("XAI_API_KEY")))
|
|
.expect("provider config read should include XAI_API_KEY");
|
|
assert_eq!(xai_key.get("isSet"), Some(&serde_json::json!(true)));
|
|
assert_ne!(
|
|
xai_key.get("value"),
|
|
Some(&serde_json::json!("xai-configured-key")),
|
|
"provider config read should not expose raw secret values"
|
|
);
|
|
|
|
let non_oauth_auth = send_custom(
|
|
conn.cx(),
|
|
"_goose/unstable/providers/config/authenticate",
|
|
serde_json::json!({ "providerId": "xai" }),
|
|
)
|
|
.await;
|
|
assert!(
|
|
non_oauth_auth.is_err(),
|
|
"native auth should reject providers without an OAuth flow"
|
|
);
|
|
|
|
Config::global().invalidate_secrets_cache();
|
|
assert!(Config::global()
|
|
.get_secret::<String>("CUSTOM_STARK_ACP_PROVIDER_API_KEY")
|
|
.is_err());
|
|
|
|
let created = send_custom(
|
|
conn.cx(),
|
|
"_goose/unstable/providers/custom/create",
|
|
serde_json::json!({
|
|
"engine": "openai_compatible",
|
|
"displayName": "Stark ACP Provider",
|
|
"apiUrl": "https://stark.example/v1",
|
|
"apiKey": "created-custom-key",
|
|
"models": ["stark-1", "stark-2"],
|
|
"supportsStreaming": true,
|
|
"headers": {
|
|
"X-Stark": "enabled"
|
|
},
|
|
"requiresAuth": true,
|
|
"catalogProviderId": "openai",
|
|
"basePath": "v1/chat/completions"
|
|
}),
|
|
)
|
|
.await
|
|
.expect("custom provider create should succeed");
|
|
let provider_id = created
|
|
.get("providerId")
|
|
.and_then(|provider_id| provider_id.as_str())
|
|
.expect("custom provider create should return providerId")
|
|
.to_string();
|
|
assert_eq!(provider_id, "custom_stark_acp_provider");
|
|
assert_eq!(
|
|
created.get("status"),
|
|
Some(&serde_json::json!({
|
|
"providerId": provider_id,
|
|
"isConfigured": true,
|
|
})),
|
|
"create should invalidate the secret cache before status checks"
|
|
);
|
|
assert_eq!(
|
|
created.get("refresh"),
|
|
Some(&serde_json::json!({
|
|
"started": [],
|
|
"skipped": [
|
|
{
|
|
"providerId": provider_id,
|
|
"reason": "does_not_support_refresh",
|
|
},
|
|
],
|
|
}))
|
|
);
|
|
|
|
let custom_provider_path = Paths::config_dir()
|
|
.join("custom_providers")
|
|
.join(format!("{provider_id}.json"));
|
|
assert!(
|
|
custom_provider_path.exists(),
|
|
"custom provider should be saved in Goose's declarative provider store"
|
|
);
|
|
let saved_provider: DeclarativeProviderConfig =
|
|
serde_json::from_str(&std::fs::read_to_string(&custom_provider_path).unwrap())
|
|
.expect("saved provider should be core-compatible declarative config");
|
|
assert_eq!(saved_provider.name, provider_id);
|
|
assert_eq!(saved_provider.display_name, "Stark ACP Provider");
|
|
assert_eq!(saved_provider.base_url, "https://stark.example/v1");
|
|
assert!(saved_provider.preserves_thinking);
|
|
assert_eq!(
|
|
saved_provider
|
|
.models
|
|
.iter()
|
|
.map(|model| model.name.as_str())
|
|
.collect::<Vec<_>>(),
|
|
vec!["stark-1", "stark-2"]
|
|
);
|
|
assert_eq!(
|
|
Config::global()
|
|
.get_secret::<String>("CUSTOM_STARK_ACP_PROVIDER_API_KEY")
|
|
.unwrap(),
|
|
"created-custom-key",
|
|
"custom provider create should write through Goose's config store"
|
|
);
|
|
assert!(
|
|
load_provider(&provider_id)
|
|
.expect("core should load the ACP-created custom provider")
|
|
.is_editable
|
|
);
|
|
|
|
let read = send_custom(
|
|
conn.cx(),
|
|
"_goose/unstable/providers/custom/read",
|
|
serde_json::json!({ "providerId": provider_id }),
|
|
)
|
|
.await
|
|
.expect("custom provider read should succeed");
|
|
assert_eq!(read.get("editable"), Some(&serde_json::json!(true)));
|
|
assert_eq!(
|
|
read.pointer("/provider"),
|
|
Some(&serde_json::json!({
|
|
"providerId": provider_id,
|
|
"engine": "openai_compatible",
|
|
"displayName": "Stark ACP Provider",
|
|
"apiUrl": "https://stark.example/v1",
|
|
"models": ["stark-1", "stark-2"],
|
|
"supportsStreaming": true,
|
|
"headers": {
|
|
"X-Stark": "enabled"
|
|
},
|
|
"requiresAuth": true,
|
|
"catalogProviderId": "openai",
|
|
"basePath": "v1/chat/completions",
|
|
"apiKeyEnv": "CUSTOM_STARK_ACP_PROVIDER_API_KEY",
|
|
"apiKeySet": true,
|
|
"preservesThinking": true,
|
|
}))
|
|
);
|
|
|
|
let inventory = send_custom(
|
|
conn.cx(),
|
|
"_goose/unstable/providers/list",
|
|
serde_json::json!({ "providerIds": [provider_id] }),
|
|
)
|
|
.await
|
|
.expect("provider inventory list should include custom provider");
|
|
assert_eq!(
|
|
inventory.pointer("/entries/0/providerType"),
|
|
Some(&serde_json::json!("Custom"))
|
|
);
|
|
assert_eq!(
|
|
inventory.pointer("/entries/0/providerId"),
|
|
Some(&serde_json::json!(provider_id))
|
|
);
|
|
|
|
let updated = send_custom(
|
|
conn.cx(),
|
|
"_goose/unstable/providers/custom/update",
|
|
serde_json::json!({
|
|
"providerId": provider_id,
|
|
"engine": "openai",
|
|
"displayName": "Stark ACP Provider Updated",
|
|
"apiUrl": "https://stark.example/openai",
|
|
"apiKey": "updated-custom-key",
|
|
"models": ["stark-3"],
|
|
"supportsStreaming": false,
|
|
"headers": {},
|
|
"requiresAuth": true,
|
|
"catalogProviderId": "zai",
|
|
"preservesThinking": false
|
|
}),
|
|
)
|
|
.await
|
|
.expect("custom provider update should succeed");
|
|
assert_eq!(
|
|
updated.get("status"),
|
|
Some(&serde_json::json!({
|
|
"providerId": provider_id,
|
|
"isConfigured": true,
|
|
})),
|
|
"update should invalidate the secret cache before status checks"
|
|
);
|
|
assert_eq!(
|
|
Config::global()
|
|
.get_secret::<String>("CUSTOM_STARK_ACP_PROVIDER_API_KEY")
|
|
.unwrap(),
|
|
"updated-custom-key",
|
|
"custom provider update should write through Goose's config store"
|
|
);
|
|
let updated_provider: DeclarativeProviderConfig =
|
|
serde_json::from_str(&std::fs::read_to_string(&custom_provider_path).unwrap())
|
|
.expect("updated provider should remain core-compatible");
|
|
assert_eq!(updated_provider.display_name, "Stark ACP Provider Updated");
|
|
assert_eq!(updated_provider.base_url, "https://stark.example/openai");
|
|
assert_eq!(
|
|
updated_provider.catalog_provider_id,
|
|
Some("zai".to_string())
|
|
);
|
|
assert_eq!(updated_provider.base_path, None);
|
|
assert_eq!(updated_provider.headers, None);
|
|
assert!(!updated_provider.preserves_thinking);
|
|
assert_eq!(
|
|
updated_provider
|
|
.models
|
|
.iter()
|
|
.map(|model| model.name.as_str())
|
|
.collect::<Vec<_>>(),
|
|
vec!["stark-3"]
|
|
);
|
|
|
|
let auth_disabled = send_custom(
|
|
conn.cx(),
|
|
"_goose/unstable/providers/custom/update",
|
|
serde_json::json!({
|
|
"providerId": provider_id,
|
|
"engine": "openai_compatible",
|
|
"displayName": "Stark ACP Provider No Auth",
|
|
"apiUrl": "https://stark.example/openai",
|
|
"apiKey": "",
|
|
"models": ["stark-3"],
|
|
"supportsStreaming": false,
|
|
"headers": {},
|
|
"requiresAuth": false,
|
|
"catalogProviderId": "zai"
|
|
}),
|
|
)
|
|
.await
|
|
.expect("custom provider auth disable should succeed");
|
|
assert_eq!(
|
|
auth_disabled.get("status"),
|
|
Some(&serde_json::json!({
|
|
"providerId": provider_id,
|
|
"isConfigured": true,
|
|
})),
|
|
"auth disable should invalidate the secret cache before status checks"
|
|
);
|
|
let no_auth_provider: DeclarativeProviderConfig =
|
|
serde_json::from_str(&std::fs::read_to_string(&custom_provider_path).unwrap())
|
|
.expect("no-auth provider should remain core-compatible");
|
|
assert!(!no_auth_provider.requires_auth);
|
|
assert_eq!(no_auth_provider.api_key_env, "");
|
|
assert!(!no_auth_provider.preserves_thinking);
|
|
assert!(
|
|
matches!(
|
|
Config::global().get_secret::<String>("CUSTOM_STARK_ACP_PROVIDER_API_KEY"),
|
|
Err(ConfigError::NotFound(_))
|
|
),
|
|
"disabling auth should delete the previously stored API key"
|
|
);
|
|
|
|
let auth_reenabled_without_key = send_custom(
|
|
conn.cx(),
|
|
"_goose/unstable/providers/custom/update",
|
|
serde_json::json!({
|
|
"providerId": provider_id,
|
|
"engine": "openai_compatible",
|
|
"displayName": "Stark ACP Provider Reauth",
|
|
"apiUrl": "https://stark.example/openai",
|
|
"apiKey": "",
|
|
"models": ["stark-3"],
|
|
"supportsStreaming": false,
|
|
"headers": {},
|
|
"requiresAuth": true,
|
|
"catalogProviderId": "zai"
|
|
}),
|
|
)
|
|
.await
|
|
.expect_err("re-enabling auth without a stored secret should fail");
|
|
assert!(
|
|
auth_reenabled_without_key
|
|
.to_string()
|
|
.contains("apiKey is required"),
|
|
"unexpected error: {auth_reenabled_without_key}"
|
|
);
|
|
assert!(
|
|
matches!(
|
|
Config::global().get_secret::<String>("CUSTOM_STARK_ACP_PROVIDER_API_KEY"),
|
|
Err(ConfigError::NotFound(_))
|
|
),
|
|
"blank re-enable should not recreate the previous API key"
|
|
);
|
|
|
|
let deleted = send_custom(
|
|
conn.cx(),
|
|
"_goose/unstable/providers/custom/delete",
|
|
serde_json::json!({ "providerId": provider_id }),
|
|
)
|
|
.await
|
|
.expect("custom provider delete should succeed");
|
|
assert_eq!(
|
|
deleted.pointer("/providerId"),
|
|
Some(&serde_json::json!(provider_id))
|
|
);
|
|
assert_eq!(
|
|
deleted.get("refresh"),
|
|
Some(&serde_json::json!({
|
|
"started": [],
|
|
"skipped": [],
|
|
}))
|
|
);
|
|
assert!(
|
|
!custom_provider_path.exists(),
|
|
"custom provider delete should remove the declarative provider file"
|
|
);
|
|
assert!(
|
|
matches!(
|
|
Config::global().get_secret::<String>("CUSTOM_STARK_ACP_PROVIDER_API_KEY"),
|
|
Err(ConfigError::NotFound(_))
|
|
),
|
|
"custom provider delete should invalidate the secret cache before later reads"
|
|
);
|
|
|
|
let deleted_status = send_custom(
|
|
conn.cx(),
|
|
"_goose/unstable/providers/config/status",
|
|
serde_json::json!({ "providerIds": [provider_id] }),
|
|
)
|
|
.await
|
|
.expect("provider config status should succeed after delete");
|
|
assert_eq!(
|
|
deleted_status.pointer("/statuses/0"),
|
|
Some(&serde_json::json!({
|
|
"providerId": provider_id,
|
|
"isConfigured": false,
|
|
}))
|
|
);
|
|
|
|
for invalid_id in [
|
|
"../escape",
|
|
"foo/bar",
|
|
".hidden",
|
|
"-bad",
|
|
"",
|
|
"Uppercase",
|
|
"has space",
|
|
] {
|
|
let read = send_custom(
|
|
conn.cx(),
|
|
"_goose/unstable/providers/custom/read",
|
|
serde_json::json!({ "providerId": invalid_id }),
|
|
)
|
|
.await;
|
|
assert!(
|
|
read.is_err(),
|
|
"invalid provider id should fail: {invalid_id:?}"
|
|
);
|
|
}
|
|
|
|
for valid_id in ["custom_openai", "openai-compat", "a1"] {
|
|
assert!(
|
|
goose::config::declarative_providers::validate_provider_id(valid_id).is_ok(),
|
|
"provider id should be valid: {valid_id}"
|
|
);
|
|
}
|
|
|
|
for (name, patch) in [
|
|
(
|
|
"ftp URL",
|
|
serde_json::json!({ "apiUrl": "ftp://example.com" }),
|
|
),
|
|
("relative URL", serde_json::json!({ "apiUrl": "/v1" })),
|
|
("empty models", serde_json::json!({ "models": [] })),
|
|
("blank models", serde_json::json!({ "models": [" ", "\n"] })),
|
|
(
|
|
"invalid header name",
|
|
serde_json::json!({ "headers": { "Bad Header": "value" } }),
|
|
),
|
|
(
|
|
"invalid header value",
|
|
serde_json::json!({ "headers": { "X-Test": "bad\r\nvalue" } }),
|
|
),
|
|
(
|
|
"unsupported engine",
|
|
serde_json::json!({ "engine": "future_engine" }),
|
|
),
|
|
] {
|
|
let mut payload = serde_json::json!({
|
|
"engine": "openai_compatible",
|
|
"displayName": format!("Invalid {name}"),
|
|
"apiUrl": "https://api.example.test/v1",
|
|
"apiKey": "secret",
|
|
"models": ["model-a"],
|
|
"headers": {},
|
|
"requiresAuth": true
|
|
});
|
|
let payload_obj = payload.as_object_mut().unwrap();
|
|
for (key, value) in patch.as_object().unwrap() {
|
|
payload_obj.insert(key.clone(), value.clone());
|
|
}
|
|
|
|
let result = send_custom(
|
|
conn.cx(),
|
|
"_goose/unstable/providers/custom/create",
|
|
payload,
|
|
)
|
|
.await;
|
|
assert!(result.is_err(), "{name} should be rejected");
|
|
}
|
|
|
|
Config::global()
|
|
.set_secret("SHARED_API_KEY", &"shared-secret")
|
|
.unwrap();
|
|
|
|
let shared = send_custom(
|
|
conn.cx(),
|
|
"_goose/unstable/providers/custom/create",
|
|
serde_json::json!({
|
|
"engine": "openai_compatible",
|
|
"displayName": "Shared Secret Test",
|
|
"apiUrl": "https://api.example.test/v1",
|
|
"apiKey": "owned-secret",
|
|
"models": ["model-a"],
|
|
"headers": {},
|
|
"requiresAuth": true
|
|
}),
|
|
)
|
|
.await
|
|
.expect("shared-secret provider create should succeed");
|
|
let shared_id = shared
|
|
.get("providerId")
|
|
.and_then(|provider_id| provider_id.as_str())
|
|
.unwrap()
|
|
.to_string();
|
|
let shared_path = Paths::config_dir()
|
|
.join("custom_providers")
|
|
.join(format!("{shared_id}.json"));
|
|
let mut shared_config: DeclarativeProviderConfig =
|
|
serde_json::from_str(&std::fs::read_to_string(&shared_path).unwrap()).unwrap();
|
|
shared_config.api_key_env = "SHARED_API_KEY".to_string();
|
|
std::fs::write(
|
|
&shared_path,
|
|
serde_json::to_string_pretty(&shared_config).unwrap(),
|
|
)
|
|
.unwrap();
|
|
Config::global().invalidate_secrets_cache();
|
|
|
|
send_custom(
|
|
conn.cx(),
|
|
"_goose/unstable/providers/custom/update",
|
|
serde_json::json!({
|
|
"providerId": shared_id,
|
|
"engine": "openai_compatible",
|
|
"displayName": "Shared Secret Test",
|
|
"apiUrl": "https://api.example.test/v1",
|
|
"models": ["model-a"],
|
|
"headers": {},
|
|
"requiresAuth": false
|
|
}),
|
|
)
|
|
.await
|
|
.expect("disabling auth should preserve shared secrets");
|
|
assert_eq!(
|
|
Config::global()
|
|
.get_secret::<String>("SHARED_API_KEY")
|
|
.unwrap(),
|
|
"shared-secret"
|
|
);
|
|
|
|
let shared_delete = send_custom(
|
|
conn.cx(),
|
|
"_goose/unstable/providers/custom/create",
|
|
serde_json::json!({
|
|
"engine": "openai_compatible",
|
|
"displayName": "Shared Secret Delete",
|
|
"apiUrl": "https://api.example.test/v1",
|
|
"apiKey": "owned-secret",
|
|
"models": ["model-a"],
|
|
"headers": {},
|
|
"requiresAuth": true
|
|
}),
|
|
)
|
|
.await
|
|
.expect("shared-delete provider create should succeed");
|
|
let shared_delete_id = shared_delete
|
|
.get("providerId")
|
|
.and_then(|provider_id| provider_id.as_str())
|
|
.unwrap()
|
|
.to_string();
|
|
let shared_delete_path = Paths::config_dir()
|
|
.join("custom_providers")
|
|
.join(format!("{shared_delete_id}.json"));
|
|
let mut shared_delete_config: DeclarativeProviderConfig =
|
|
serde_json::from_str(&std::fs::read_to_string(&shared_delete_path).unwrap()).unwrap();
|
|
shared_delete_config.api_key_env = "SHARED_API_KEY".to_string();
|
|
std::fs::write(
|
|
&shared_delete_path,
|
|
serde_json::to_string_pretty(&shared_delete_config).unwrap(),
|
|
)
|
|
.unwrap();
|
|
Config::global().invalidate_secrets_cache();
|
|
|
|
send_custom(
|
|
conn.cx(),
|
|
"_goose/unstable/providers/custom/delete",
|
|
serde_json::json!({ "providerId": shared_delete_id }),
|
|
)
|
|
.await
|
|
.expect("deleting provider should preserve shared secrets");
|
|
assert_eq!(
|
|
Config::global()
|
|
.get_secret::<String>("SHARED_API_KEY")
|
|
.unwrap(),
|
|
"shared-secret"
|
|
);
|
|
});
|
|
}
|