Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions crates/wake-core/src/adapters/devin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
//! `<数据根>/cli/sessions.db` 全明文 SQLite。数据根服从 XDG——
//! `$XDG_DATA_HOME/devin`,缺省 `~/.local/share/devin`(macOS 同样走 XDG
//! 形态,实测);`~/Library/Application Support/devin` 是桌面 Electron 壳的
//! profile 目录,作为历史候选一并探。候选按序取第一个真有库文件的,都没有
//! 时回落 XDG 语义下的默认位置(env 候选里真有库才采信,存在但空的目录
//! 不能遮掉默认根——与其他 adapter 同一约定)。
//! profile 目录,作为历史候选一并探;Windows 的 Devin Desktop 把库放在
//! `%APPDATA%\devin`(从 `home_dir()` 派生,与 cursor_ide 同一约定:不走
//! `dirs::config_dir()`,否则 `WAKE_HOME` 改道对它无效),同样探。候选按序取
//! 第一个真有库文件的,都没有时回落 XDG 语义下的默认位置(env 候选里真有库
//! 才采信,存在但空的目录不能遮掉默认根——与其他 adapter 同一约定)。
//!
//! `sessions(id,title,working_directory,model,agent_mode,created_at,
//! last_activity_at,hidden,main_chain_id,…)` + `message_nodes(row_id,
Expand Down Expand Up @@ -108,9 +110,13 @@ impl DevinAdapter {
.join("Library")
.join("Application Support")
.join("devin");
// Windows 的 Devin Desktop: %APPDATA%\devin。Mac/Linux 上该目录不会
// 存在,无条件作候选无害(与 app_support 同理),也让三平台的契约测试
// 都能覆盖这条解析
let appdata = home.join("AppData").join("Roaming").join("devin");
let hit = xdg
.iter()
.chain([&local, &app_support])
.chain([&local, &app_support, &appdata])
.find(|dir| db_in(dir).is_file())
.cloned();
let root = hit.unwrap_or_else(|| xdg.unwrap_or(local));
Expand Down
67 changes: 67 additions & 0 deletions crates/wake-core/tests/devin_default_root.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//! Devin 默认数据根的解析契约。`DevinAdapter::new()` 的候选链
//! (XDG → `~/.local/share` → macOS Application Support → Windows
//! `%APPDATA%`)依赖进程级 `WAKE_HOME`/`XDG_DATA_HOME`,因此整个文件只有
//! 一个用例、串行覆盖两种布局(与 mcp_stdio/remote_sync 同一约定):
//! 1. 只有 `%APPDATA%\devin\cli\sessions.db`(真实 Windows 机器的形态)时,
//! 默认解析必须落到它——此前 Windows 上没有任何候选,整家会话空列;
//! 2. XDG 形态(`~/.local/share/devin`)同时存在时仍按序优先(跨平台回归)。

use std::fs;
use wake_core::adapters::devin::DevinAdapter;
use wake_core::adapters::AgentAdapter;

mod common;

#[test]
fn devin_default_root_resolves_windows_appdata() {
// 独立假家:进程级 env 之外不受 adapter_contracts 的共享 fixture 影响
let home = tempfile::Builder::new()
.prefix("wake-devin-default-root-")
.tempdir()
.expect("create fake home");
let appdata_dir = home
.path()
.join("AppData")
.join("Roaming")
.join("devin")
.join("cli");
fs::create_dir_all(&appdata_dir).expect("mkdir AppData devin cli");
let appdata_db = appdata_dir.join("sessions.db");
common::build_devin_db(&appdata_db);

// 老家目录清理 + 改道,先只给 AppData 一份(Windows 真机形态)
std::env::set_var("WAKE_HOME", home.path());
std::env::set_var("HOME", home.path());
std::env::remove_var("XDG_DATA_HOME");

let adapter = DevinAdapter::new();
assert_eq!(
adapter.data_roots(),
vec![appdata_db.clone()],
"只有 %APPDATA%\\devin 一份库时,默认解析必须命中它"
);
let ids: Vec<String> = adapter
.list_session_files()
.expect("devin list")
.into_iter()
.map(|r| r.native_id)
.collect();
assert_eq!(ids, vec!["dv-0001", "dv-0002"], "AppData 布局整家可列");

// XDG 形态出现后仍按候选序优先:老平台用户的会话不能换根消失
let xdg_dir = home
.path()
.join(".local")
.join("share")
.join("devin")
.join("cli");
fs::create_dir_all(&xdg_dir).expect("mkdir XDG devin cli");
let xdg_db = xdg_dir.join("sessions.db");
common::build_devin_db(&xdg_db);
let adapter = DevinAdapter::new();
assert_eq!(
adapter.data_roots(),
vec![xdg_db],
"XDG 与 AppData 并存时按候选序取 XDG 形态"
);
}