fix(account): keep a DeepSeek or keyed local route when signing in

`codewhale login` switched the local route to the managed Codewhale provider
whenever the configured provider equalled ProviderKind::default(), intended
as "never configured". DeepSeek is the default provider, so a person who
explicitly chose DeepSeek, or stored a DeepSeek key, was silently moved to
`provider = "codewhale"`, `model = "auto"` on sign-in. Found while
reproducing a stale-sign-in report: a QA login against a loopback account
API rewrote the operator's own config this way (restored by hand).

Sign-in now takes over the route only when the config file names no
provider and the current provider has no local key (config, secret store
or environment, via the existing resolve_local_key). An explicit provider,
including deepseek, is kept.

Evidence: cargo test -p codewhale-cli --lib cloud::tests::login_:
with fix 4 passed; 0 failed. Against the previous cloud.rs the two new
regressions fail (2 passed; 2 failed), so they pin the defect.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
CodeWhale BotandClaude Opus 5.5 committed 2026-10-06 19:20:03 -07:00
1 parent 00b84da0e4
commit 8fb9e13e77
2 files changed
+47 -4

No files matched your search

+16 -4
View File
@@ -2350,9 +2350,21 @@ pub(crate) fn reject_inline_api_key(api_key: Option<&str>) -> Result<()> {
/// On account sign-in, point a never-configured local route at the managed
/// Codewhale provider so chat works immediately. A provider the user chose
/// explicitly is left alone.
fn select_managed_route_on_login<W: Write>(config: &mut ConfigStore, out: &mut W) -> Result<()> {
if config.config.provider == ProviderKind::default() {
/// explicitly, or one that already has a local key (config, secret store or
/// environment), is left alone: the default provider is DeepSeek, so equality
/// with the default cannot tell "never configured" from "chose DeepSeek".
fn select_managed_route_on_login<W: Write>(
config: &mut ConfigStore,
provider_secrets: &Secrets,
out: &mut W,
) -> Result<()> {
let explicit_provider = config
.original_body()
.and_then(|body| body.parse::<toml::Table>().ok())
.is_some_and(|table| table.contains_key("provider"));
let has_local_key =
resolve_local_key(config, provider_secrets, config.config.provider)?.is_some();
if config.config.provider == ProviderKind::default() && !explicit_provider && !has_local_key {
config.config.provider = ProviderKind::Codewhale;
config.config.model = Some("auto".to_string());
config.save()?;
@@ -2416,7 +2428,7 @@ fn run_with<T: CloudTransport, W: Write>(
client.poll_device(&device, Duration::from_secs(login.timeout_seconds), sleeper)?;
let user = client.me()?;
write_account(out, "Signed in to Codewhale.", profile, api_base, &user)?;
select_managed_route_on_login(config, out)?;
select_managed_route_on_login(config, provider_secrets, out)?;
Ok(())
}
CloudCommand::Status => match client.load_auth()? {
+31
View File
@@ -635,6 +635,37 @@ fn login_keeps_explicitly_configured_route() {
assert_eq!(saved.config.model, None);
}
#[test]
fn login_keeps_an_explicit_deepseek_route() {
// DeepSeek is the default provider; choosing it explicitly must survive sign-in.
let (temp, _) = test_config();
std::fs::write(temp.path().join("config.toml"), "provider = \"deepseek\"\n").unwrap();
let mut config = ConfigStore::load(Some(temp.path().join("config.toml"))).unwrap();
let (secrets, _) = test_secrets();
let output = run_login(&mut config, &secrets);
assert!(
output.contains("Keeping your configured deepseek route."),
"{output}"
);
let saved = ConfigStore::load(Some(temp.path().join("config.toml"))).unwrap();
assert_eq!(saved.config.provider, ProviderKind::Deepseek);
assert_eq!(saved.config.model, None);
}
#[test]
fn login_keeps_the_default_route_when_it_has_a_local_key() {
let (temp, mut config) = test_config();
let (secrets, _) = test_secrets();
secrets.set("deepseek", "sk-local-never-print").unwrap();
let output = run_login(&mut config, &secrets);
assert!(
output.contains("Keeping your configured deepseek route."),
"{output}"
);
let saved = ConfigStore::load(Some(temp.path().join("config.toml"))).unwrap();
assert_eq!(saved.config.provider, ProviderKind::Deepseek);
}
#[test]
fn cloud_sessions_are_isolated_by_profile_and_api_origin() {
let (secrets, _) = test_secrets();