use super::dto::{
DesktopDaemonRequest, DesktopLifecycleActionDto, DesktopMemoryActionRequest,
DesktopMemoryDraftRequest, DesktopMetadataDto, DesktopRecordLookupRequest, DesktopRouteRequest,
DesktopSessionItem, DesktopWakeupRequest, DesktopWorkbenchRequest,
};
use super::errors::DesktopErrorKind;
use super::helpers::{
build_continue_command, delete_session_file, parse_csv_items, parse_file_list,
};
use super::service::DesktopService;
use crate::domain::{MemoryLifecycleState, MemoryScope, OutputFormat, TargetTool, WakeupProfile};
use std::fs;
use tempfile::tempdir;
fn setup_workspace() -> (tempfile::TempDir, std::path::PathBuf, std::path::PathBuf) {
let temp = tempdir().unwrap();
let vault_root = temp.path().join("vault");
let repo_root = temp.path().join("repo");
fs::create_dir_all(vault_root.join("10-Projects")).unwrap();
fs::create_dir_all(&repo_root).unwrap();
fs::write(
vault_root.join("10-Projects/spool.md"),
"---\ntitle: spool\nmemory_type: project\n---\n\nDesktop facade test note.\n",
)
.unwrap();
let config_path = temp.path().join("spool.toml");
fs::write(
&config_path,
format!(
"[vault]\nroot = \"{}\"\n\n[output]\ndefault_format = \"markdown\"\nmax_chars = 4000\nmax_notes = 8\n\n[[projects]]\nid = \"spool\"\nname = \"spool\"\nrepo_paths = [\"{}\"]\nnote_roots = [\"10-Projects\"]\n",
vault_root.display(),
repo_root.display(),
),
)
.unwrap();
(temp, config_path, repo_root)
}
#[test]
fn parse_helpers_should_split_csv_and_multiline_inputs() {
assert_eq!(parse_csv_items("a, b, ,c"), vec!["a", "b", "c"]);
assert_eq!(
parse_file_list("a.rs, b.rs\n c.rs"),
vec!["a.rs", "b.rs", "c.rs"]
);
}
#[test]
fn service_should_run_context_workflow() {
let (_temp, config_path, repo_root) = setup_workspace();
let response = DesktopService::new()
.run_context(DesktopRouteRequest {
config_path,
vault_root_override: None,
cwd: repo_root,
task: "summarize current project context".to_string(),
files: vec!["src/lib.rs".to_string()],
target: TargetTool::Claude,
format: OutputFormat::Markdown,
})
.unwrap();
assert_eq!(response.used_format, OutputFormat::Markdown);
assert_eq!(response.bundle.route.debug.note_count, 1);
assert!(response.rendered.contains("spool") || !response.rendered.is_empty());
}
#[test]
fn service_should_build_wakeup_workflow() {
let (_temp, config_path, repo_root) = setup_workspace();
let response = DesktopService::new()
.build_wakeup(DesktopWakeupRequest {
config_path,
vault_root_override: None,
cwd: repo_root,
task: "prepare restart packet".to_string(),
files: vec![],
target: TargetTool::Claude,
profile: WakeupProfile::Project,
})
.unwrap();
assert_eq!(response.packet.profile, WakeupProfile::Project);
assert_eq!(response.bundle.route.debug.note_count, 1);
}
#[test]
fn service_should_wrap_validation_errors_in_input_envelope() {
let error = DesktopService::new()
.run_context(DesktopRouteRequest {
config_path: std::path::PathBuf::from("/missing/spool.toml"),
vault_root_override: None,
cwd: std::env::temp_dir(),
task: String::new(),
files: vec![],
target: TargetTool::Claude,
format: OutputFormat::Markdown,
})
.unwrap_err();
assert_eq!(error.kind, DesktopErrorKind::Input);
assert!(error.message.contains("Config 不存在") || error.message.contains("Task"));
}
#[test]
fn service_should_expose_lifecycle_workbench_record_history_and_actions() {
let (_temp, config_path, _repo_root) = setup_workspace();
let draft = DesktopMemoryDraftRequest {
config_path: config_path.clone(),
title: "测试偏好".to_string(),
summary: "先 smoke 再收口".to_string(),
memory_type: "workflow".to_string(),
scope: MemoryScope::User,
source_ref: "session:1".to_string(),
project_id: None,
user_id: Some("long".to_string()),
sensitivity: None,
metadata: DesktopMetadataDto {
actor: Some("desktop".to_string()),
reason: Some("captured from desktop workflow".to_string()),
evidence_refs: vec!["session:1".to_string()],
},
entities: Vec::new(),
tags: Vec::new(),
triggers: Vec::new(),
related_files: Vec::new(),
related_records: Vec::new(),
supersedes: None,
applies_to: Vec::new(),
valid_until: None,
};
let created = DesktopService::new().propose_memory(draft).unwrap();
assert_eq!(created.entry.record.state, MemoryLifecycleState::Candidate);
assert_eq!(created.payload["summary"]["kind"], "propose");
let workbench = DesktopService::new()
.load_workbench(DesktopWorkbenchRequest {
config_path: config_path.clone(),
daemon: Some(DesktopDaemonRequest {
enabled: false,
daemon_bin: None,
}),
})
.unwrap();
assert_eq!(workbench.snapshot.pending_review.len(), 1);
let record = DesktopService::new()
.get_record(DesktopRecordLookupRequest {
config_path: config_path.clone(),
record_id: created.entry.record_id.clone(),
daemon: None,
})
.unwrap()
.unwrap();
assert_eq!(record.record.record.title, "测试偏好");
let action = DesktopService::new()
.apply_memory_action(DesktopMemoryActionRequest {
config_path: config_path.clone(),
record_id: created.entry.record_id.clone(),
action: DesktopLifecycleActionDto::Accept,
metadata: DesktopMetadataDto::default(),
})
.unwrap();
assert_eq!(action.entry.record.state, MemoryLifecycleState::Accepted);
assert_eq!(action.payload["action"], "accept");
let history = DesktopService::new()
.get_history(DesktopRecordLookupRequest {
config_path,
record_id: created.entry.record_id,
daemon: None,
})
.unwrap();
assert_eq!(history.history.len(), 2);
assert_eq!(history.payload["record_id"], history.record_id);
}
#[test]
fn session_action_helpers_should_build_resume_commands_and_delete_files() {
let claude = DesktopSessionItem {
provider: "claude".to_string(),
session_id: "claude:demo-1".to_string(),
title: "Claude".to_string(),
summary: None,
prompt_preview: None,
cwd: Some("/tmp/project".to_string()),
source_path: None,
project_path: None,
updated_at: "2026-04-16T12:00:00Z".to_string(),
record_count: 0,
pending_review_count: 0,
wakeup_ready_count: 0,
titles: Vec::new(),
memory_types: Vec::new(),
};
assert_eq!(
build_continue_command(&claude).as_deref(),
Some("cd '/tmp/project' && claude -r \"demo-1\"")
);
let codex = DesktopSessionItem {
provider: "codex".to_string(),
session_id: "codex:demo-2".to_string(),
title: "Codex".to_string(),
summary: None,
prompt_preview: None,
cwd: None,
source_path: None,
project_path: None,
updated_at: "2026-04-16T12:00:00Z".to_string(),
record_count: 0,
pending_review_count: 0,
wakeup_ready_count: 0,
titles: Vec::new(),
memory_types: Vec::new(),
};
assert_eq!(
build_continue_command(&codex).as_deref(),
Some("codex resume \"demo-2\"")
);
let unsupported = DesktopSessionItem {
provider: "spool".to_string(),
session_id: "spool:session:demo".to_string(),
title: "spool".to_string(),
summary: None,
prompt_preview: None,
cwd: None,
source_path: None,
project_path: None,
updated_at: "2026-04-16T12:00:00Z".to_string(),
record_count: 0,
pending_review_count: 0,
wakeup_ready_count: 0,
titles: Vec::new(),
memory_types: Vec::new(),
};
assert!(build_continue_command(&unsupported).is_none());
let temp = tempdir().unwrap();
let session_file = temp.path().join("session.jsonl");
fs::write(&session_file, "demo").unwrap();
let deletable = DesktopSessionItem {
provider: "claude".to_string(),
session_id: "claude:delete-demo".to_string(),
title: "delete".to_string(),
summary: None,
prompt_preview: None,
cwd: None,
source_path: Some(session_file.display().to_string()),
project_path: None,
updated_at: "2026-04-16T12:00:00Z".to_string(),
record_count: 0,
pending_review_count: 0,
wakeup_ready_count: 0,
titles: Vec::new(),
memory_types: Vec::new(),
};
delete_session_file(&deletable).unwrap();
assert!(!session_file.exists());
}