fix: restore API key config resolution and clean runtime artifacts

This commit is contained in:
Hunter Bown
2026-02-16 11:37:14 -06:00
parent 6c05c3b3ac
commit 4212ba9f26
17 changed files with 221 additions and 170 deletions
View File
+4
View File
@@ -55,3 +55,7 @@ AI_HANDOFF.md
.codex/
docs/rlm-paper.txt
.context/
# Local runtime state
.deepseek/
session_*.json
+9
View File
@@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.3.17] - 2026-02-16
### Fixed
- Config loading now expands `~` in `DEEPSEEK_CONFIG_PATH` and `--config` paths.
- When `DEEPSEEK_CONFIG_PATH` points to a missing file, config loading now falls back to `~/.deepseek/config.toml` if it exists.
### Changed
- Removed committed transient runtime artifacts (`session_*.json`, `.deepseek/trusted`) and added ignore rules to prevent re-commit.
## [0.3.16] - 2026-02-15
### Added
Generated
+1 -1
View File
@@ -726,7 +726,7 @@ dependencies = [
[[package]]
name = "deepseek-tui"
version = "0.3.16"
version = "0.3.17"
dependencies = [
"anyhow",
"arboard",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deepseek-tui"
version = "0.3.16"
version = "0.3.17"
edition = "2024"
description = "Unofficial DeepSeek CLI - Just run 'deepseek' to start chatting"
license = "MIT"
-16
View File
@@ -1,16 +0,0 @@
{
"schema_version": 1,
"metadata": {
"id": "9f574f3d-c6f6-436e-9967-aaaba56148a2",
"title": "New Session",
"created_at": "2026-02-16T15:52:42.502684Z",
"updated_at": "2026-02-16T15:52:42.502684Z",
"message_count": 0,
"total_tokens": 0,
"model": "deepseek-v3.2",
"workspace": "/var/folders/gc/lw1tgpk97z51d30mcvbhyb400000gn/T/.tmpTtSsZw",
"mode": "AGENT"
},
"messages": [],
"system_prompt": null
}
-16
View File
@@ -1,16 +0,0 @@
{
"schema_version": 1,
"metadata": {
"id": "a9cab85f-7ea0-44f8-a6dc-88aee23462ab",
"title": "New Session",
"created_at": "2026-02-16T15:59:08.213118Z",
"updated_at": "2026-02-16T15:59:08.213118Z",
"message_count": 0,
"total_tokens": 0,
"model": "deepseek-v3.2",
"workspace": "/var/folders/gc/lw1tgpk97z51d30mcvbhyb400000gn/T/.tmpT6EfcG",
"mode": "AGENT"
},
"messages": [],
"system_prompt": null
}
-16
View File
@@ -1,16 +0,0 @@
{
"schema_version": 1,
"metadata": {
"id": "fa7764c1-15ba-4b5a-bce8-e4f939e1f838",
"title": "New Session",
"created_at": "2026-02-16T16:00:56.854336Z",
"updated_at": "2026-02-16T16:00:56.854336Z",
"message_count": 0,
"total_tokens": 0,
"model": "deepseek-v3.2",
"workspace": "/var/folders/gc/lw1tgpk97z51d30mcvbhyb400000gn/T/.tmpe0lg3x",
"mode": "AGENT"
},
"messages": [],
"system_prompt": null
}
-16
View File
@@ -1,16 +0,0 @@
{
"schema_version": 1,
"metadata": {
"id": "8d3f306c-70d3-46d9-8007-10c638966197",
"title": "New Session",
"created_at": "2026-02-16T16:16:55.890410Z",
"updated_at": "2026-02-16T16:16:55.890410Z",
"message_count": 0,
"total_tokens": 0,
"model": "deepseek-v3.2",
"workspace": "/var/folders/gc/lw1tgpk97z51d30mcvbhyb400000gn/T/.tmpUidsSQ",
"mode": "AGENT"
},
"messages": [],
"system_prompt": null
}
-16
View File
@@ -1,16 +0,0 @@
{
"schema_version": 1,
"metadata": {
"id": "159c08f3-9aab-435c-9f2b-370a40667710",
"title": "New Session",
"created_at": "2026-02-16T16:21:28.631837Z",
"updated_at": "2026-02-16T16:21:28.631837Z",
"message_count": 0,
"total_tokens": 0,
"model": "deepseek-v3.2",
"workspace": "/var/folders/gc/lw1tgpk97z51d30mcvbhyb400000gn/T/.tmpeUxI8I",
"mode": "AGENT"
},
"messages": [],
"system_prompt": null
}
-16
View File
@@ -1,16 +0,0 @@
{
"schema_version": 1,
"metadata": {
"id": "4e6f3623-30d7-431e-8b11-d8549dac8c13",
"title": "New Session",
"created_at": "2026-02-16T16:30:41.499125Z",
"updated_at": "2026-02-16T16:30:41.499125Z",
"message_count": 0,
"total_tokens": 0,
"model": "deepseek-v3.2",
"workspace": "/var/folders/gc/lw1tgpk97z51d30mcvbhyb400000gn/T/.tmpjykcjh",
"mode": "AGENT"
},
"messages": [],
"system_prompt": null
}
-16
View File
@@ -1,16 +0,0 @@
{
"schema_version": 1,
"metadata": {
"id": "0c1e5eb4-e426-4d8a-a622-80b1939e5083",
"title": "New Session",
"created_at": "2026-02-16T16:34:48.304398Z",
"updated_at": "2026-02-16T16:34:48.304398Z",
"message_count": 0,
"total_tokens": 0,
"model": "deepseek-v3.2",
"workspace": "/var/folders/gc/lw1tgpk97z51d30mcvbhyb400000gn/T/.tmp1bRsZy",
"mode": "AGENT"
},
"messages": [],
"system_prompt": null
}
-16
View File
@@ -1,16 +0,0 @@
{
"schema_version": 1,
"metadata": {
"id": "ce96ab6c-6b3c-45fb-8c69-f7659e50cb03",
"title": "New Session",
"created_at": "2026-02-16T16:45:48.643707Z",
"updated_at": "2026-02-16T16:45:48.643707Z",
"message_count": 0,
"total_tokens": 0,
"model": "deepseek-v3.2",
"workspace": "/var/folders/gc/lw1tgpk97z51d30mcvbhyb400000gn/T/.tmpkQGlZi",
"mode": "AGENT"
},
"messages": [],
"system_prompt": null
}
-16
View File
@@ -1,16 +0,0 @@
{
"schema_version": 1,
"metadata": {
"id": "3ea22ecd-cb0a-485b-ba2e-9f9e02d1ec74",
"title": "New Session",
"created_at": "2026-02-16T16:56:57.987857Z",
"updated_at": "2026-02-16T16:56:57.987857Z",
"message_count": 0,
"total_tokens": 0,
"model": "deepseek-v3.2",
"workspace": "/var/folders/gc/lw1tgpk97z51d30mcvbhyb400000gn/T/.tmpOWW4ty",
"mode": "AGENT"
},
"messages": [],
"system_prompt": null
}
-16
View File
@@ -1,16 +0,0 @@
{
"schema_version": 1,
"metadata": {
"id": "206fc731-8181-4693-8c27-1fbc9cc8032e",
"title": "New Session",
"created_at": "2026-02-16T16:57:39.210126Z",
"updated_at": "2026-02-16T16:57:39.210126Z",
"message_count": 0,
"total_tokens": 0,
"model": "deepseek-v3.2",
"workspace": "/var/folders/gc/lw1tgpk97z51d30mcvbhyb400000gn/T/.tmpTNf6nn",
"mode": "AGENT"
},
"messages": [],
"system_prompt": null
}
+101 -2
View File
@@ -218,7 +218,88 @@ mod tests {
use crate::config::Config;
use crate::tui::app::{App, TuiOptions};
use crate::tui::approval::ApprovalMode;
use std::env;
use std::ffi::OsString;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
use std::sync::{Mutex, OnceLock};
use std::time::{SystemTime, UNIX_EPOCH};
struct EnvGuard {
home: Option<OsString>,
userprofile: Option<OsString>,
deepseek_config_path: Option<OsString>,
}
impl EnvGuard {
fn new(home: &Path) -> Self {
let home_str = OsString::from(home.as_os_str());
let config_path = home.join(".deepseek").join("config.toml");
let config_str = OsString::from(config_path.as_os_str());
let home_prev = env::var_os("HOME");
let userprofile_prev = env::var_os("USERPROFILE");
let deepseek_config_prev = env::var_os("DEEPSEEK_CONFIG_PATH");
// Safety: test-only environment mutation guarded by a global mutex.
unsafe {
env::set_var("HOME", &home_str);
env::set_var("USERPROFILE", &home_str);
env::set_var("DEEPSEEK_CONFIG_PATH", &config_str);
}
Self {
home: home_prev,
userprofile: userprofile_prev,
deepseek_config_path: deepseek_config_prev,
}
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
if let Some(value) = self.home.take() {
// Safety: test-only environment mutation guarded by a global mutex.
unsafe {
env::set_var("HOME", value);
}
} else {
// Safety: test-only environment mutation guarded by a global mutex.
unsafe {
env::remove_var("HOME");
}
}
if let Some(value) = self.userprofile.take() {
// Safety: test-only environment mutation guarded by a global mutex.
unsafe {
env::set_var("USERPROFILE", value);
}
} else {
// Safety: test-only environment mutation guarded by a global mutex.
unsafe {
env::remove_var("USERPROFILE");
}
}
if let Some(value) = self.deepseek_config_path.take() {
// Safety: test-only environment mutation guarded by a global mutex.
unsafe {
env::set_var("DEEPSEEK_CONFIG_PATH", value);
}
} else {
// Safety: test-only environment mutation guarded by a global mutex.
unsafe {
env::remove_var("DEEPSEEK_CONFIG_PATH");
}
}
}
}
fn env_lock() -> &'static Mutex<()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
}
fn create_test_app() -> App {
let options = TuiOptions {
@@ -364,15 +445,33 @@ mod tests {
#[test]
fn test_logout_clears_api_key_state() {
let _lock = env_lock().lock().unwrap();
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let temp_root = env::temp_dir().join(format!(
"deepseek-cli-logout-test-{}-{}",
std::process::id(),
nanos
));
fs::create_dir_all(&temp_root).unwrap();
let _guard = EnvGuard::new(&temp_root);
let config_path = temp_root.join(".deepseek").join("config.toml");
fs::create_dir_all(config_path.parent().unwrap()).unwrap();
fs::write(&config_path, "api_key = \"test-key\"\n").unwrap();
let mut app = create_test_app();
// Note: This test may fail if API key is not set in environment
// but the state changes should still occur
let result = logout(&mut app);
assert!(result.message.is_some());
assert_eq!(app.onboarding, OnboardingState::ApiKey);
assert!(app.onboarding_needs_api_key);
assert!(app.api_key_input.is_empty());
assert_eq!(app.api_key_cursor, 0);
let updated = fs::read_to_string(config_path).unwrap();
assert!(!updated.contains("api_key"));
}
#[test]
+105 -6
View File
@@ -123,7 +123,7 @@ impl Config {
/// # Ok::<(), anyhow::Error>(())
/// ```
pub fn load(path: Option<PathBuf>, profile: Option<&str>) -> Result<Self> {
let path = path.or_else(default_config_path);
let path = resolve_load_config_path(path);
let mut config = if let Some(path) = path.as_ref() {
if path.exists() {
let contents = fs::read_to_string(path)
@@ -337,14 +337,52 @@ impl Config {
// === Defaults ===
fn default_config_path() -> Option<PathBuf> {
if let Ok(path) = std::env::var("DEEPSEEK_CONFIG_PATH")
&& !path.trim().is_empty()
{
return Some(PathBuf::from(path));
}
env_config_path().or_else(home_config_path)
}
fn home_config_path() -> Option<PathBuf> {
dirs::home_dir().map(|home| home.join(".deepseek").join("config.toml"))
}
fn env_config_path() -> Option<PathBuf> {
if let Ok(path) = std::env::var("DEEPSEEK_CONFIG_PATH") {
let trimmed = path.trim();
if !trimmed.is_empty() {
return Some(expand_path(trimmed));
}
}
None
}
fn expand_pathbuf(path: PathBuf) -> PathBuf {
if let Some(raw) = path.to_str() {
return expand_path(raw);
}
path
}
fn resolve_load_config_path(path: Option<PathBuf>) -> Option<PathBuf> {
if let Some(path) = path {
return Some(expand_pathbuf(path));
}
if let Some(path) = env_config_path() {
if path.exists() {
return Some(path);
}
if let Some(home_path) = home_config_path()
&& home_path.exists()
{
return Some(home_path);
}
return Some(path);
}
home_config_path()
}
fn default_managed_config_path() -> Option<PathBuf> {
#[cfg(unix)]
{
@@ -850,6 +888,67 @@ mod tests {
Ok(())
}
#[test]
fn test_load_uses_tilde_expanded_deepseek_config_path() -> Result<()> {
let _lock = env_lock().lock().unwrap();
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let temp_root = env::temp_dir().join(format!(
"deepseek-cli-load-tilde-test-{}-{}",
std::process::id(),
nanos
));
fs::create_dir_all(&temp_root)?;
let _guard = EnvGuard::new(&temp_root);
let config_path = temp_root.join(".custom-deepseek").join("config.toml");
ensure_parent_dir(&config_path)?;
fs::write(&config_path, "api_key = \"test-key\"\n")?;
// Safety: test-only environment mutation guarded by a global mutex.
unsafe {
env::set_var("DEEPSEEK_CONFIG_PATH", "~/.custom-deepseek/config.toml");
}
let config = Config::load(None, None)?;
assert_eq!(config.api_key.as_deref(), Some("test-key"));
Ok(())
}
#[test]
fn test_load_falls_back_to_home_config_when_env_path_missing() -> Result<()> {
let _lock = env_lock().lock().unwrap();
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let temp_root = env::temp_dir().join(format!(
"deepseek-cli-load-fallback-test-{}-{}",
std::process::id(),
nanos
));
fs::create_dir_all(&temp_root)?;
let _guard = EnvGuard::new(&temp_root);
let home_config = temp_root.join(".deepseek").join("config.toml");
ensure_parent_dir(&home_config)?;
fs::write(&home_config, "api_key = \"home-key\"\n")?;
// Safety: test-only environment mutation guarded by a global mutex.
unsafe {
env::set_var(
"DEEPSEEK_CONFIG_PATH",
temp_root.join("missing-config.toml").as_os_str(),
);
}
let config = Config::load(None, None)?;
assert_eq!(config.api_key.as_deref(), Some("home-key"));
Ok(())
}
#[test]
fn test_nonexistent_profile_error() {
let mut profiles = HashMap::new();