Migrate on auto-load

This commit is contained in:
Richard Feldman
2025-12-11 00:22:38 -05:00
parent 8d96a699b3
commit ee4faede38
7 changed files with 107 additions and 419 deletions

View File

@@ -1,11 +1,14 @@
use credentials_provider::CredentialsProvider;
use gpui::App;
use util::ResultExt as _;
const ANTHROPIC_EXTENSION_ID: &str = "anthropic";
const ANTHROPIC_PROVIDER_ID: &str = "anthropic";
const ANTHROPIC_DEFAULT_API_URL: &str = "https://api.anthropic.com";
/// Migrates Anthropic API credentials from the old built-in provider location
/// to the new extension-based location.
///
/// This should only be called during auto-install of the extension.
pub fn migrate_anthropic_credentials_if_needed(extension_id: &str, cx: &mut App) {
if extension_id != ANTHROPIC_EXTENSION_ID {
return;
@@ -19,17 +22,7 @@ pub fn migrate_anthropic_credentials_if_needed(extension_id: &str, cx: &mut App)
let credentials_provider = <dyn CredentialsProvider>::global(cx);
cx.spawn(async move |cx| {
let existing_credential = credentials_provider
.read_credentials(&extension_credential_key, &cx)
.await
.ok()
.flatten();
if existing_credential.is_some() {
log::debug!("Anthropic extension already has credentials, skipping migration");
return;
}
// Read from old location
let old_credential = credentials_provider
.read_credentials(ANTHROPIC_DEFAULT_API_URL, &cx)
.await
@@ -40,8 +33,8 @@ pub fn migrate_anthropic_credentials_if_needed(extension_id: &str, cx: &mut App)
Some((_, key_bytes)) => match String::from_utf8(key_bytes) {
Ok(key) if !key.is_empty() => key,
Ok(_) => {
log::debug!("Existing Anthropic API key is empty, marking as migrated");
String::new()
log::debug!("Existing Anthropic API key is empty, nothing to migrate");
return;
}
Err(_) => {
log::error!("Failed to decode Anthropic API key as UTF-8");
@@ -49,20 +42,11 @@ pub fn migrate_anthropic_credentials_if_needed(extension_id: &str, cx: &mut App)
}
},
None => {
log::debug!("No existing Anthropic API key found, marking as migrated");
String::new()
log::debug!("No existing Anthropic API key found to migrate");
return;
}
};
if api_key.is_empty() {
// Write empty credentials as a marker that migration was attempted
credentials_provider
.write_credentials(&extension_credential_key, "Bearer", b"", &cx)
.await
.log_err();
return;
}
log::info!("Migrating existing Anthropic API key to Anthropic extension");
match credentials_provider
@@ -105,57 +89,7 @@ mod tests {
}
#[gpui::test]
async fn test_skips_migration_if_extension_already_has_credentials(cx: &mut TestAppContext) {
let old_api_key = "sk-ant-old-key";
let existing_key = "sk-ant-existing-key";
cx.write_credentials(ANTHROPIC_DEFAULT_API_URL, "Bearer", old_api_key.as_bytes());
cx.write_credentials(
"extension-llm-anthropic:anthropic",
"Bearer",
existing_key.as_bytes(),
);
cx.update(|cx| {
migrate_anthropic_credentials_if_needed(ANTHROPIC_EXTENSION_ID, cx);
});
cx.run_until_parked();
let credentials = cx.read_credentials("extension-llm-anthropic:anthropic");
let (_, password) = credentials.unwrap();
assert_eq!(
String::from_utf8(password).unwrap(),
existing_key,
"Should not overwrite existing credentials"
);
}
#[gpui::test]
async fn test_skips_migration_if_empty_marker_exists(cx: &mut TestAppContext) {
let old_api_key = "sk-ant-old-key";
// Old credentials exist
cx.write_credentials(ANTHROPIC_DEFAULT_API_URL, "Bearer", old_api_key.as_bytes());
// But empty marker already exists (from previous migration attempt)
cx.write_credentials("extension-llm-anthropic:anthropic", "Bearer", b"");
cx.update(|cx| {
migrate_anthropic_credentials_if_needed(ANTHROPIC_EXTENSION_ID, cx);
});
cx.run_until_parked();
let credentials = cx.read_credentials("extension-llm-anthropic:anthropic");
let (_, password) = credentials.unwrap();
assert!(
password.is_empty(),
"Should not overwrite empty marker with old credentials"
);
}
#[gpui::test]
async fn test_writes_empty_marker_if_no_old_credentials(cx: &mut TestAppContext) {
async fn test_no_migration_if_no_old_credentials(cx: &mut TestAppContext) {
cx.update(|cx| {
migrate_anthropic_credentials_if_needed(ANTHROPIC_EXTENSION_ID, cx);
});
@@ -164,12 +98,9 @@ mod tests {
let credentials = cx.read_credentials("extension-llm-anthropic:anthropic");
assert!(
credentials.is_some(),
"Should write empty credentials as migration marker"
credentials.is_none(),
"Should not create credentials if none existed"
);
let (username, password) = credentials.unwrap();
assert_eq!(username, "Bearer");
assert!(password.is_empty(), "Password should be empty marker");
}
#[gpui::test]

View File

@@ -1,11 +1,14 @@
use credentials_provider::CredentialsProvider;
use gpui::App;
use std::path::PathBuf;
use util::ResultExt as _;
const COPILOT_CHAT_EXTENSION_ID: &str = "copilot-chat";
const COPILOT_CHAT_PROVIDER_ID: &str = "copilot-chat";
/// Migrates Copilot OAuth credentials from the GitHub Copilot config files
/// to the new extension-based credential location.
///
/// This should only be called during auto-install of the extension.
pub fn migrate_copilot_credentials_if_needed(extension_id: &str, cx: &mut App) {
if extension_id != COPILOT_CHAT_EXTENSION_ID {
return;
@@ -18,27 +21,12 @@ pub fn migrate_copilot_credentials_if_needed(extension_id: &str, cx: &mut App) {
let credentials_provider = <dyn CredentialsProvider>::global(cx);
cx.spawn(async move |cx| {
let existing_credential = credentials_provider
.read_credentials(&credential_key, &cx)
.await
.ok()
.flatten();
if existing_credential.is_some() {
log::debug!("Copilot Chat extension already has credentials, skipping migration");
return;
}
cx.spawn(async move |_cx| {
// Read from copilot config files
let oauth_token = match read_copilot_oauth_token().await {
Some(token) if !token.is_empty() => token,
_ => {
log::debug!("No existing Copilot OAuth token found, marking as migrated");
// Write empty credentials as a marker that migration was attempted
credentials_provider
.write_credentials(&credential_key, "api_key", b"", &cx)
.await
.log_err();
log::debug!("No existing Copilot OAuth token found to migrate");
return;
}
};
@@ -46,7 +34,7 @@ pub fn migrate_copilot_credentials_if_needed(extension_id: &str, cx: &mut App) {
log::info!("Migrating existing Copilot OAuth token to Copilot Chat extension");
match credentials_provider
.write_credentials(&credential_key, "api_key", oauth_token.as_bytes(), &cx)
.write_credentials(&credential_key, "api_key", oauth_token.as_bytes(), &_cx)
.await
{
Ok(()) => {
@@ -193,31 +181,6 @@ mod tests {
assert_eq!(token, Some("github_token".to_string()));
}
#[gpui::test]
async fn test_skips_migration_if_extension_already_has_credentials(cx: &mut TestAppContext) {
let existing_token = "existing_oauth_token";
cx.write_credentials(
"extension-llm-copilot-chat:copilot-chat",
"api_key",
existing_token.as_bytes(),
);
cx.update(|cx| {
migrate_copilot_credentials_if_needed(COPILOT_CHAT_EXTENSION_ID, cx);
});
cx.run_until_parked();
let credentials = cx.read_credentials("extension-llm-copilot-chat:copilot-chat");
let (_, password) = credentials.unwrap();
assert_eq!(
String::from_utf8(password).unwrap(),
existing_token,
"Should not overwrite existing credentials"
);
}
#[gpui::test]
async fn test_skips_migration_for_other_extensions(cx: &mut TestAppContext) {
cx.update(|cx| {
@@ -235,9 +198,7 @@ mod tests {
// Note: Unlike the other migrations, copilot migration reads from the filesystem
// (copilot config files), not from the credentials provider. In tests, these files
// don't exist, and the smol async filesystem operations don't integrate well with
// the GPUI test executor's run_until_parked(). So we test the original behavior:
// no config files = no credentials written.
// don't exist, so no migration occurs.
#[gpui::test]
async fn test_no_credentials_when_no_copilot_config_exists(cx: &mut TestAppContext) {
cx.update(|cx| {
@@ -247,11 +208,9 @@ mod tests {
cx.run_until_parked();
let credentials = cx.read_credentials("extension-llm-copilot-chat:copilot-chat");
// The async task that would write the marker doesn't complete in tests
// because smol filesystem operations use a different executor
assert!(
credentials.is_none(),
"No credentials should be written when copilot config doesn't exist (in test environment)"
"No credentials should be written when copilot config doesn't exist"
);
}
}

View File

@@ -219,6 +219,8 @@ pub struct ExtensionStore {
pub enum ExtensionOperation {
Upgrade,
Install,
/// Auto-install from settings - triggers legacy LLM provider migrations
AutoInstall,
Remove,
}
@@ -755,7 +757,7 @@ impl ExtensionStore {
}
this.update(cx, |this, cx| {
this.install_latest_extension(extension_id.clone(), cx);
this.auto_install_latest_extension(extension_id.clone(), cx);
})
.ok();
}
@@ -910,13 +912,11 @@ impl ExtensionStore {
this.update(cx, |this, cx| this.reload(Some(extension_id.clone()), cx))?
.await;
if let ExtensionOperation::Install = operation {
if matches!(
operation,
ExtensionOperation::Install | ExtensionOperation::AutoInstall
) {
this.update(cx, |this, cx| {
// Check for legacy LLM provider migration
if let Some(manifest) = this.extension_manifest_for_id(&extension_id) {
migrate_legacy_llm_provider_env_var(&manifest, cx);
}
cx.emit(Event::ExtensionInstalled(extension_id.clone()));
if let Some(events) = ExtensionEvents::try_global(cx)
&& let Some(manifest) = this.extension_manifest_for_id(&extension_id)
@@ -926,15 +926,26 @@ impl ExtensionStore {
});
}
// Run extension-specific migrations
copilot_migration::migrate_copilot_credentials_if_needed(&extension_id, cx);
anthropic_migration::migrate_anthropic_credentials_if_needed(&extension_id, cx);
google_ai_migration::migrate_google_ai_credentials_if_needed(&extension_id, cx);
openai_migration::migrate_openai_credentials_if_needed(&extension_id, cx);
open_router_migration::migrate_open_router_credentials_if_needed(
&extension_id,
cx,
);
// Run legacy LLM provider migrations only for auto-installed extensions
if matches!(operation, ExtensionOperation::AutoInstall) {
if let Some(manifest) = this.extension_manifest_for_id(&extension_id) {
migrate_legacy_llm_provider_env_var(&manifest, cx);
}
copilot_migration::migrate_copilot_credentials_if_needed(&extension_id, cx);
anthropic_migration::migrate_anthropic_credentials_if_needed(
&extension_id,
cx,
);
google_ai_migration::migrate_google_ai_credentials_if_needed(
&extension_id,
cx,
);
openai_migration::migrate_openai_credentials_if_needed(&extension_id, cx);
open_router_migration::migrate_open_router_credentials_if_needed(
&extension_id,
cx,
);
}
})
.ok();
}
@@ -944,6 +955,24 @@ impl ExtensionStore {
}
pub fn install_latest_extension(&mut self, extension_id: Arc<str>, cx: &mut Context<Self>) {
self.install_latest_extension_with_operation(extension_id, ExtensionOperation::Install, cx);
}
/// Auto-install an extension, triggering legacy LLM provider migrations.
fn auto_install_latest_extension(&mut self, extension_id: Arc<str>, cx: &mut Context<Self>) {
self.install_latest_extension_with_operation(
extension_id,
ExtensionOperation::AutoInstall,
cx,
);
}
fn install_latest_extension_with_operation(
&mut self,
extension_id: Arc<str>,
operation: ExtensionOperation,
cx: &mut Context<Self>,
) {
let schema_versions = schema_version_range();
let wasm_api_versions = wasm_api_version_range(ReleaseChannel::global(cx));
@@ -966,13 +995,8 @@ impl ExtensionStore {
return;
};
self.install_or_upgrade_extension_at_endpoint(
extension_id,
url,
ExtensionOperation::Install,
cx,
)
.detach_and_log_err(cx);
self.install_or_upgrade_extension_at_endpoint(extension_id, url, operation, cx)
.detach_and_log_err(cx);
}
pub fn upgrade_extension(
@@ -1171,11 +1195,6 @@ impl ExtensionStore {
this.update(cx, |this, cx| this.reload(None, cx))?.await;
this.update(cx, |this, cx| {
// Run migration for legacy LLM provider env vars
if let Some(manifest) = this.extension_manifest_for_id(&extension_id) {
migrate_legacy_llm_provider_env_var(&manifest, cx);
}
cx.emit(Event::ExtensionInstalled(extension_id.clone()));
if let Some(events) = ExtensionEvents::try_global(cx)
&& let Some(manifest) = this.extension_manifest_for_id(&extension_id)

View File

@@ -1,11 +1,14 @@
use credentials_provider::CredentialsProvider;
use gpui::App;
use util::ResultExt as _;
const GOOGLE_AI_EXTENSION_ID: &str = "google-ai";
const GOOGLE_AI_PROVIDER_ID: &str = "google-ai";
const GOOGLE_AI_DEFAULT_API_URL: &str = "https://generativelanguage.googleapis.com";
/// Migrates Google AI API credentials from the old built-in provider location
/// to the new extension-based location.
///
/// This should only be called during auto-install of the extension.
pub fn migrate_google_ai_credentials_if_needed(extension_id: &str, cx: &mut App) {
if extension_id != GOOGLE_AI_EXTENSION_ID {
return;
@@ -19,17 +22,7 @@ pub fn migrate_google_ai_credentials_if_needed(extension_id: &str, cx: &mut App)
let credentials_provider = <dyn CredentialsProvider>::global(cx);
cx.spawn(async move |cx| {
let existing_credential = credentials_provider
.read_credentials(&extension_credential_key, &cx)
.await
.ok()
.flatten();
if existing_credential.is_some() {
log::debug!("Google AI extension already has credentials, skipping migration");
return;
}
// Read from old location
let old_credential = credentials_provider
.read_credentials(GOOGLE_AI_DEFAULT_API_URL, &cx)
.await
@@ -40,8 +33,8 @@ pub fn migrate_google_ai_credentials_if_needed(extension_id: &str, cx: &mut App)
Some((_, key_bytes)) => match String::from_utf8(key_bytes) {
Ok(key) if !key.is_empty() => key,
Ok(_) => {
log::debug!("Existing Google AI API key is empty, marking as migrated");
String::new()
log::debug!("Existing Google AI API key is empty, nothing to migrate");
return;
}
Err(_) => {
log::error!("Failed to decode Google AI API key as UTF-8");
@@ -49,20 +42,11 @@ pub fn migrate_google_ai_credentials_if_needed(extension_id: &str, cx: &mut App)
}
},
None => {
log::debug!("No existing Google AI API key found, marking as migrated");
String::new()
log::debug!("No existing Google AI API key found to migrate");
return;
}
};
if api_key.is_empty() {
// Write empty credentials as a marker that migration was attempted
credentials_provider
.write_credentials(&extension_credential_key, "Bearer", b"", &cx)
.await
.log_err();
return;
}
log::info!("Migrating existing Google AI API key to Google AI extension");
match credentials_provider
@@ -105,57 +89,7 @@ mod tests {
}
#[gpui::test]
async fn test_skips_migration_if_extension_already_has_credentials(cx: &mut TestAppContext) {
let old_api_key = "AIzaSy-old-key";
let existing_key = "AIzaSy-existing-key";
cx.write_credentials(GOOGLE_AI_DEFAULT_API_URL, "Bearer", old_api_key.as_bytes());
cx.write_credentials(
"extension-llm-google-ai:google-ai",
"Bearer",
existing_key.as_bytes(),
);
cx.update(|cx| {
migrate_google_ai_credentials_if_needed(GOOGLE_AI_EXTENSION_ID, cx);
});
cx.run_until_parked();
let credentials = cx.read_credentials("extension-llm-google-ai:google-ai");
let (_, password) = credentials.unwrap();
assert_eq!(
String::from_utf8(password).unwrap(),
existing_key,
"Should not overwrite existing credentials"
);
}
#[gpui::test]
async fn test_skips_migration_if_empty_marker_exists(cx: &mut TestAppContext) {
let old_api_key = "AIzaSy-old-key";
// Old credentials exist
cx.write_credentials(GOOGLE_AI_DEFAULT_API_URL, "Bearer", old_api_key.as_bytes());
// But empty marker already exists (from previous migration attempt)
cx.write_credentials("extension-llm-google-ai:google-ai", "Bearer", b"");
cx.update(|cx| {
migrate_google_ai_credentials_if_needed(GOOGLE_AI_EXTENSION_ID, cx);
});
cx.run_until_parked();
let credentials = cx.read_credentials("extension-llm-google-ai:google-ai");
let (_, password) = credentials.unwrap();
assert!(
password.is_empty(),
"Should not overwrite empty marker with old credentials"
);
}
#[gpui::test]
async fn test_writes_empty_marker_if_no_old_credentials(cx: &mut TestAppContext) {
async fn test_no_migration_if_no_old_credentials(cx: &mut TestAppContext) {
cx.update(|cx| {
migrate_google_ai_credentials_if_needed(GOOGLE_AI_EXTENSION_ID, cx);
});
@@ -164,12 +98,9 @@ mod tests {
let credentials = cx.read_credentials("extension-llm-google-ai:google-ai");
assert!(
credentials.is_some(),
"Should write empty credentials as migration marker"
credentials.is_none(),
"Should not create credentials if none existed"
);
let (username, password) = credentials.unwrap();
assert_eq!(username, "Bearer");
assert!(password.is_empty(), "Password should be empty marker");
}
#[gpui::test]

View File

@@ -1,11 +1,14 @@
use credentials_provider::CredentialsProvider;
use gpui::App;
use util::ResultExt as _;
const OPEN_ROUTER_EXTENSION_ID: &str = "openrouter";
const OPEN_ROUTER_PROVIDER_ID: &str = "openrouter";
const OPEN_ROUTER_DEFAULT_API_URL: &str = "https://openrouter.ai/api/v1";
/// Migrates OpenRouter API credentials from the old built-in provider location
/// to the new extension-based location.
///
/// This should only be called during auto-install of the extension.
pub fn migrate_open_router_credentials_if_needed(extension_id: &str, cx: &mut App) {
if extension_id != OPEN_ROUTER_EXTENSION_ID {
return;
@@ -19,17 +22,7 @@ pub fn migrate_open_router_credentials_if_needed(extension_id: &str, cx: &mut Ap
let credentials_provider = <dyn CredentialsProvider>::global(cx);
cx.spawn(async move |cx| {
let existing_credential = credentials_provider
.read_credentials(&extension_credential_key, &cx)
.await
.ok()
.flatten();
if existing_credential.is_some() {
log::debug!("OpenRouter extension already has credentials, skipping migration");
return;
}
// Read from old location
let old_credential = credentials_provider
.read_credentials(OPEN_ROUTER_DEFAULT_API_URL, &cx)
.await
@@ -40,8 +33,8 @@ pub fn migrate_open_router_credentials_if_needed(extension_id: &str, cx: &mut Ap
Some((_, key_bytes)) => match String::from_utf8(key_bytes) {
Ok(key) if !key.is_empty() => key,
Ok(_) => {
log::debug!("Existing OpenRouter API key is empty, marking as migrated");
String::new()
log::debug!("Existing OpenRouter API key is empty, nothing to migrate");
return;
}
Err(_) => {
log::error!("Failed to decode OpenRouter API key as UTF-8");
@@ -49,20 +42,11 @@ pub fn migrate_open_router_credentials_if_needed(extension_id: &str, cx: &mut Ap
}
},
None => {
log::debug!("No existing OpenRouter API key found, marking as migrated");
String::new()
log::debug!("No existing OpenRouter API key found to migrate");
return;
}
};
if api_key.is_empty() {
// Write empty credentials as a marker that migration was attempted
credentials_provider
.write_credentials(&extension_credential_key, "Bearer", b"", &cx)
.await
.log_err();
return;
}
log::info!("Migrating existing OpenRouter API key to OpenRouter extension");
match credentials_provider
@@ -105,65 +89,7 @@ mod tests {
}
#[gpui::test]
async fn test_skips_migration_if_extension_already_has_credentials(cx: &mut TestAppContext) {
let old_api_key = "sk-or-old-key";
let existing_key = "sk-or-existing-key";
cx.write_credentials(
OPEN_ROUTER_DEFAULT_API_URL,
"Bearer",
old_api_key.as_bytes(),
);
cx.write_credentials(
"extension-llm-openrouter:openrouter",
"Bearer",
existing_key.as_bytes(),
);
cx.update(|cx| {
migrate_open_router_credentials_if_needed(OPEN_ROUTER_EXTENSION_ID, cx);
});
cx.run_until_parked();
let credentials = cx.read_credentials("extension-llm-openrouter:openrouter");
let (_, password) = credentials.unwrap();
assert_eq!(
String::from_utf8(password).unwrap(),
existing_key,
"Should not overwrite existing credentials"
);
}
#[gpui::test]
async fn test_skips_migration_if_empty_marker_exists(cx: &mut TestAppContext) {
let old_api_key = "sk-or-old-key";
// Old credentials exist
cx.write_credentials(
OPEN_ROUTER_DEFAULT_API_URL,
"Bearer",
old_api_key.as_bytes(),
);
// But empty marker already exists (from previous migration attempt)
cx.write_credentials("extension-llm-openrouter:openrouter", "Bearer", b"");
cx.update(|cx| {
migrate_open_router_credentials_if_needed(OPEN_ROUTER_EXTENSION_ID, cx);
});
cx.run_until_parked();
let credentials = cx.read_credentials("extension-llm-openrouter:openrouter");
let (_, password) = credentials.unwrap();
assert!(
password.is_empty(),
"Should not overwrite empty marker with old credentials"
);
}
#[gpui::test]
async fn test_writes_empty_marker_if_no_old_credentials(cx: &mut TestAppContext) {
async fn test_no_migration_if_no_old_credentials(cx: &mut TestAppContext) {
cx.update(|cx| {
migrate_open_router_credentials_if_needed(OPEN_ROUTER_EXTENSION_ID, cx);
});
@@ -172,12 +98,9 @@ mod tests {
let credentials = cx.read_credentials("extension-llm-openrouter:openrouter");
assert!(
credentials.is_some(),
"Should write empty credentials as migration marker"
credentials.is_none(),
"Should not create credentials if none existed"
);
let (username, password) = credentials.unwrap();
assert_eq!(username, "Bearer");
assert!(password.is_empty(), "Password should be empty marker");
}
#[gpui::test]

View File

@@ -1,11 +1,14 @@
use credentials_provider::CredentialsProvider;
use gpui::App;
use util::ResultExt as _;
const OPENAI_EXTENSION_ID: &str = "openai";
const OPENAI_PROVIDER_ID: &str = "openai";
const OPENAI_DEFAULT_API_URL: &str = "https://api.openai.com/v1";
/// Migrates OpenAI API credentials from the old built-in provider location
/// to the new extension-based location.
///
/// This should only be called during auto-install of the extension.
pub fn migrate_openai_credentials_if_needed(extension_id: &str, cx: &mut App) {
if extension_id != OPENAI_EXTENSION_ID {
return;
@@ -19,17 +22,7 @@ pub fn migrate_openai_credentials_if_needed(extension_id: &str, cx: &mut App) {
let credentials_provider = <dyn CredentialsProvider>::global(cx);
cx.spawn(async move |cx| {
let existing_credential = credentials_provider
.read_credentials(&extension_credential_key, &cx)
.await
.ok()
.flatten();
if existing_credential.is_some() {
log::debug!("OpenAI extension already has credentials, skipping migration");
return;
}
// Read from old location
let old_credential = credentials_provider
.read_credentials(OPENAI_DEFAULT_API_URL, &cx)
.await
@@ -40,8 +33,8 @@ pub fn migrate_openai_credentials_if_needed(extension_id: &str, cx: &mut App) {
Some((_, key_bytes)) => match String::from_utf8(key_bytes) {
Ok(key) if !key.is_empty() => key,
Ok(_) => {
log::debug!("Existing OpenAI API key is empty, marking as migrated");
String::new()
log::debug!("Existing OpenAI API key is empty, nothing to migrate");
return;
}
Err(_) => {
log::error!("Failed to decode OpenAI API key as UTF-8");
@@ -49,20 +42,11 @@ pub fn migrate_openai_credentials_if_needed(extension_id: &str, cx: &mut App) {
}
},
None => {
log::debug!("No existing OpenAI API key found, marking as migrated");
String::new()
log::debug!("No existing OpenAI API key found to migrate");
return;
}
};
if api_key.is_empty() {
// Write empty credentials as a marker that migration was attempted
credentials_provider
.write_credentials(&extension_credential_key, "Bearer", b"", &cx)
.await
.log_err();
return;
}
log::info!("Migrating existing OpenAI API key to OpenAI extension");
match credentials_provider
@@ -105,57 +89,7 @@ mod tests {
}
#[gpui::test]
async fn test_skips_migration_if_extension_already_has_credentials(cx: &mut TestAppContext) {
let old_api_key = "sk-old-key";
let existing_key = "sk-existing-key";
cx.write_credentials(OPENAI_DEFAULT_API_URL, "Bearer", old_api_key.as_bytes());
cx.write_credentials(
"extension-llm-openai:openai",
"Bearer",
existing_key.as_bytes(),
);
cx.update(|cx| {
migrate_openai_credentials_if_needed(OPENAI_EXTENSION_ID, cx);
});
cx.run_until_parked();
let credentials = cx.read_credentials("extension-llm-openai:openai");
let (_, password) = credentials.unwrap();
assert_eq!(
String::from_utf8(password).unwrap(),
existing_key,
"Should not overwrite existing credentials"
);
}
#[gpui::test]
async fn test_skips_migration_if_empty_marker_exists(cx: &mut TestAppContext) {
let old_api_key = "sk-old-key";
// Old credentials exist
cx.write_credentials(OPENAI_DEFAULT_API_URL, "Bearer", old_api_key.as_bytes());
// But empty marker already exists (from previous migration attempt)
cx.write_credentials("extension-llm-openai:openai", "Bearer", b"");
cx.update(|cx| {
migrate_openai_credentials_if_needed(OPENAI_EXTENSION_ID, cx);
});
cx.run_until_parked();
let credentials = cx.read_credentials("extension-llm-openai:openai");
let (_, password) = credentials.unwrap();
assert!(
password.is_empty(),
"Should not overwrite empty marker with old credentials"
);
}
#[gpui::test]
async fn test_writes_empty_marker_if_no_old_credentials(cx: &mut TestAppContext) {
async fn test_no_migration_if_no_old_credentials(cx: &mut TestAppContext) {
cx.update(|cx| {
migrate_openai_credentials_if_needed(OPENAI_EXTENSION_ID, cx);
});
@@ -164,12 +98,9 @@ mod tests {
let credentials = cx.read_credentials("extension-llm-openai:openai");
assert!(
credentials.is_some(),
"Should write empty credentials as migration marker"
credentials.is_none(),
"Should not create credentials if none existed"
);
let (username, password) = credentials.unwrap();
assert_eq!(username, "Bearer");
assert!(password.is_empty(), "Password should be empty marker");
}
#[gpui::test]

View File

@@ -428,10 +428,7 @@ impl ExtensionProviderConfigurationView {
.log_err()
.flatten();
// Treat empty credentials as not authenticated (used as migration marker)
let has_credentials = credentials
.map(|(_, password)| !password.is_empty())
.unwrap_or(false);
let has_credentials = credentials.is_some();
// Update authentication state based on stored credentials
cx.update(|cx| {
@@ -539,10 +536,7 @@ impl ExtensionProviderConfigurationView {
.log_err()
.flatten();
// Treat empty credentials as not authenticated (used as migration marker)
let has_credentials = credentials
.map(|(_, password)| !password.is_empty())
.unwrap_or(false);
let has_credentials = credentials.is_some();
cx.update(|cx| {
state.update(cx, |state, cx| {