Skip to content

Commit c58052b

Browse files
committed
fix: improve open-workspace errors and add daemon RPC parity tests
1 parent 36498c6 commit c58052b

12 files changed

Lines changed: 343 additions & 140 deletions

File tree

memory/decisions.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,3 +197,10 @@ Type: preference
197197
Event: User selected Orbit-only path for mobile remote architecture and requested canonical plan updates away from custom Cloudflare bridge implementation.
198198
Action: Rewrote `docs/mobile-ios-cloudflare-blueprint.md` to Orbit-only architecture, setup flows, settings model, transport refactor, and implementation milestones; removed custom Worker/DO protocol/envelope sections.
199199
Rule: For current mobile rollout, plan and implementation should target Orbit integration only (hosted and self-host modes), not a custom bridge protocol/service.
200+
201+
## 2026-02-07 17:39
202+
Context: Daemon parity hardening follow-up
203+
Type: decision
204+
Event: Added daemon-side RPC parity coverage for recently extracted workspace/prompts/local-usage adapters and improved open-app failure diagnostics.
205+
Action: Added RPC tests in `src-tauri/src/bin/codex_monitor_daemon.rs` for `add_clone`, `prompts_list`, and `local_usage_snapshot` routing; updated `open_workspace_in_core` in `src-tauri/src/shared/workspaces_core.rs` to include bounded stdout/stderr snippets in non-zero exit errors.
206+
Rule: Keep daemon adapter parity guarded by RPC-level tests for representative workspace/prompts/local-usage methods, and preserve process-output context in open-app failure errors.

src-tauri/src/bin/codex_monitor_daemon.rs

Lines changed: 173 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@ use backend::events::{AppServerEvent, EventSink, TerminalExit, TerminalOutput};
7676
use shared::codex_core::CodexLoginCancelState;
7777
use shared::prompts_core::{self, CustomPromptEntry};
7878
use shared::{
79-
codex_aux_core, codex_core, files_core, git_core, git_ui_core, local_usage_core,
80-
settings_core, workspaces_core, worktree_core,
79+
codex_aux_core, codex_core, files_core, git_core, git_ui_core, local_usage_core, settings_core,
80+
workspaces_core, worktree_core,
8181
};
8282
use storage::{read_settings, read_workspaces};
8383
use types::{
@@ -784,8 +784,13 @@ impl DaemonState {
784784
workspace_id: String,
785785
sha: String,
786786
) -> Result<Vec<GitCommitDiff>, String> {
787-
git_ui_core::get_git_commit_diff_core(&self.workspaces, &self.app_settings, workspace_id, sha)
788-
.await
787+
git_ui_core::get_git_commit_diff_core(
788+
&self.workspaces,
789+
&self.app_settings,
790+
workspace_id,
791+
sha,
792+
)
793+
.await
789794
}
790795

791796
async fn get_git_remote(&self, workspace_id: String) -> Result<Option<String>, String> {
@@ -860,8 +865,12 @@ impl DaemonState {
860865
workspace_id: String,
861866
pr_number: u64,
862867
) -> Result<Vec<GitHubPullRequestComment>, String> {
863-
git_ui_core::get_github_pull_request_comments_core(&self.workspaces, workspace_id, pr_number)
864-
.await
868+
git_ui_core::get_github_pull_request_comments_core(
869+
&self.workspaces,
870+
workspace_id,
871+
pr_number,
872+
)
873+
.await
865874
}
866875

867876
async fn list_git_branches(&self, workspace_id: String) -> Result<Value, String> {
@@ -881,8 +890,12 @@ impl DaemonState {
881890
}
882891

883892
async fn prompts_workspace_dir(&self, workspace_id: String) -> Result<String, String> {
884-
prompts_core::prompts_workspace_dir_core(&self.workspaces, &self.settings_path, workspace_id)
885-
.await
893+
prompts_core::prompts_workspace_dir_core(
894+
&self.workspaces,
895+
&self.settings_path,
896+
workspace_id,
897+
)
898+
.await
886899
}
887900

888901
async fn prompts_global_dir(&self, workspace_id: String) -> Result<String, String> {
@@ -1104,7 +1117,6 @@ fn emit_background_thread_hide(event_sink: &DaemonEventSink, workspace_id: &str,
11041117
});
11051118
}
11061119

1107-
11081120
fn send_notification_fallback_inner(title: String, body: String) -> Result<(), String> {
11091121
#[cfg(all(target_os = "macos", debug_assertions))]
11101122
{
@@ -2120,6 +2132,158 @@ async fn handle_client(
21202132
write_task.abort();
21212133
}
21222134

2135+
#[cfg(test)]
2136+
mod tests {
2137+
use super::*;
2138+
use crate::types::WorkspaceKind;
2139+
use serde_json::json;
2140+
use std::future::Future;
2141+
use std::path::PathBuf;
2142+
use std::time::{SystemTime, UNIX_EPOCH};
2143+
2144+
fn run_async_test<F>(future: F)
2145+
where
2146+
F: Future<Output = ()>,
2147+
{
2148+
tokio::runtime::Builder::new_current_thread()
2149+
.enable_all()
2150+
.build()
2151+
.expect("runtime")
2152+
.block_on(future);
2153+
}
2154+
2155+
fn make_temp_dir(prefix: &str) -> PathBuf {
2156+
let unique = SystemTime::now()
2157+
.duration_since(UNIX_EPOCH)
2158+
.expect("time")
2159+
.as_nanos();
2160+
let dir = std::env::temp_dir().join(format!(
2161+
"codex-monitor-{prefix}-{}-{unique}",
2162+
std::process::id()
2163+
));
2164+
std::fs::create_dir_all(&dir).expect("create temp dir");
2165+
dir
2166+
}
2167+
2168+
fn test_state(data_dir: &std::path::Path) -> DaemonState {
2169+
let (tx, _rx) = broadcast::channel::<DaemonEvent>(32);
2170+
DaemonState {
2171+
data_dir: data_dir.to_path_buf(),
2172+
workspaces: Mutex::new(HashMap::new()),
2173+
sessions: Mutex::new(HashMap::new()),
2174+
storage_path: data_dir.join("workspaces.json"),
2175+
settings_path: data_dir.join("settings.json"),
2176+
app_settings: Mutex::new(AppSettings::default()),
2177+
event_sink: DaemonEventSink { tx },
2178+
codex_login_cancels: Mutex::new(HashMap::new()),
2179+
}
2180+
}
2181+
2182+
async fn insert_workspace(state: &DaemonState, workspace_id: &str, workspace_path: &str) {
2183+
let entry = WorkspaceEntry {
2184+
id: workspace_id.to_string(),
2185+
name: "Workspace".to_string(),
2186+
path: workspace_path.to_string(),
2187+
codex_bin: None,
2188+
kind: WorkspaceKind::Main,
2189+
parent_id: None,
2190+
worktree: None,
2191+
settings: WorkspaceSettings {
2192+
codex_home: Some(format!("{workspace_path}/.codex-home")),
2193+
..WorkspaceSettings::default()
2194+
},
2195+
};
2196+
state
2197+
.workspaces
2198+
.lock()
2199+
.await
2200+
.insert(workspace_id.to_string(), entry);
2201+
}
2202+
2203+
#[test]
2204+
fn rpc_add_clone_uses_workspace_core_validation() {
2205+
run_async_test(async {
2206+
let tmp = make_temp_dir("rpc-add-clone");
2207+
let state = test_state(&tmp);
2208+
2209+
let err = handle_rpc_request(
2210+
&state,
2211+
"add_clone",
2212+
json!({
2213+
"sourceWorkspaceId": "source",
2214+
"copiesFolder": tmp.to_string_lossy().to_string(),
2215+
"copyName": " "
2216+
}),
2217+
"daemon-test".to_string(),
2218+
)
2219+
.await
2220+
.expect_err("expected validation error");
2221+
2222+
assert_eq!(err, "Copy name is required.");
2223+
let _ = std::fs::remove_dir_all(&tmp);
2224+
});
2225+
}
2226+
2227+
#[test]
2228+
fn rpc_prompts_list_reads_workspace_prompts() {
2229+
run_async_test(async {
2230+
let tmp = make_temp_dir("rpc-prompts-list");
2231+
let workspace_id = "ws-prompts";
2232+
let workspace_dir = tmp.join("workspace");
2233+
std::fs::create_dir_all(&workspace_dir).expect("create workspace dir");
2234+
2235+
let state = test_state(&tmp);
2236+
insert_workspace(&state, workspace_id, &workspace_dir.to_string_lossy()).await;
2237+
2238+
let prompts_dir = tmp.join("workspaces").join(workspace_id).join("prompts");
2239+
std::fs::create_dir_all(&prompts_dir).expect("create prompts dir");
2240+
std::fs::write(prompts_dir.join("review.md"), "Prompt body").expect("write prompt");
2241+
2242+
let result = handle_rpc_request(
2243+
&state,
2244+
"prompts_list",
2245+
json!({ "workspaceId": workspace_id }),
2246+
"daemon-test".to_string(),
2247+
)
2248+
.await
2249+
.expect("prompts_list should succeed");
2250+
2251+
let prompts = result.as_array().expect("array result");
2252+
assert!(
2253+
prompts.iter().any(|entry| {
2254+
entry
2255+
.get("name")
2256+
.and_then(Value::as_str)
2257+
.is_some_and(|name| name == "review")
2258+
}),
2259+
"expected prompts_list to include workspace prompt"
2260+
);
2261+
let _ = std::fs::remove_dir_all(&tmp);
2262+
});
2263+
}
2264+
2265+
#[test]
2266+
fn rpc_local_usage_snapshot_returns_snapshot_shape() {
2267+
run_async_test(async {
2268+
let tmp = make_temp_dir("rpc-local-usage");
2269+
let state = test_state(&tmp);
2270+
2271+
let result = handle_rpc_request(
2272+
&state,
2273+
"local_usage_snapshot",
2274+
json!({ "days": 7 }),
2275+
"daemon-test".to_string(),
2276+
)
2277+
.await
2278+
.expect("local_usage_snapshot should succeed");
2279+
2280+
assert!(result.get("days").and_then(Value::as_array).is_some());
2281+
assert!(result.get("totals").is_some());
2282+
let _ = std::fs::remove_dir_all(&tmp);
2283+
});
2284+
}
2285+
}
2286+
21232287
fn main() {
21242288
let config = match parse_args() {
21252289
Ok(config) => config,

src-tauri/src/codex/mod.rs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ pub(crate) mod args;
88
pub(crate) mod config;
99
pub(crate) mod home;
1010

11+
use crate::backend::app_server::spawn_workspace_session as spawn_workspace_session_inner;
1112
pub(crate) use crate::backend::app_server::WorkspaceSession;
1213
use crate::backend::events::AppServerEvent;
13-
use crate::backend::app_server::spawn_workspace_session as spawn_workspace_session_inner;
1414
use crate::event_sink::TauriEventSink;
1515
use crate::remote_backend;
1616
use crate::shared::codex_core;
@@ -411,12 +411,7 @@ pub(crate) async fn codex_login(
411411
.await;
412412
}
413413

414-
codex_core::codex_login_core(
415-
&state.sessions,
416-
&state.codex_login_cancels,
417-
workspace_id,
418-
)
419-
.await
414+
codex_core::codex_login_core(&state.sessions, &state.codex_login_cancels, workspace_id).await
420415
}
421416

422417
#[tauri::command]
@@ -515,7 +510,9 @@ pub(crate) async fn get_commit_message_prompt(
515510
return Err("No changes to generate commit message for".to_string());
516511
}
517512

518-
Ok(crate::shared::codex_aux_core::build_commit_message_prompt(&diff))
513+
Ok(crate::shared::codex_aux_core::build_commit_message_prompt(
514+
&diff,
515+
))
519516
}
520517

521518
#[tauri::command]

src-tauri/src/git/mod.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,11 @@ pub(crate) async fn get_workspace_diff(
113113
workspace_id: &str,
114114
state: &State<'_, AppState>,
115115
) -> Result<String, String> {
116-
let repo_root =
117-
git_ui_core::resolve_repo_root_for_workspace_core(&state.workspaces, workspace_id.to_string())
118-
.await?;
116+
let repo_root = git_ui_core::resolve_repo_root_for_workspace_core(
117+
&state.workspaces,
118+
workspace_id.to_string(),
119+
)
120+
.await?;
119121
git_ui_core::collect_workspace_diff_core(&repo_root)
120122
}
121123

src-tauri/src/shared/codex_aux_core.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,9 @@ pub(crate) async fn codex_doctor_core(
129129
command.stdout(std::process::Stdio::piped());
130130
command.stderr(std::process::Stdio::piped());
131131
let app_server_ok = match timeout(Duration::from_secs(5), command.output()).await {
132-
Ok(result) => result.map(|output| output.status.success()).unwrap_or(false),
132+
Ok(result) => result
133+
.map(|output| output.status.success())
134+
.unwrap_or(false),
133135
Err(_) => false,
134136
};
135137
let (node_ok, node_version, node_details) = {
@@ -181,7 +183,11 @@ pub(crate) async fn codex_doctor_core(
181183
}
182184
}
183185
},
184-
Err(_) => (false, None, Some("Timed out while checking Node.".to_string())),
186+
Err(_) => (
187+
false,
188+
None,
189+
Some("Timed out while checking Node.".to_string()),
190+
),
185191
}
186192
};
187193
let details = if app_server_ok {

src-tauri/src/shared/local_usage_core.rs

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -282,10 +282,11 @@ fn scan_file(
282282
continue;
283283
}
284284

285-
let info = payload.and_then(|payload| payload.get("info")).and_then(|v| v.as_object());
285+
let info = payload
286+
.and_then(|payload| payload.get("info"))
287+
.and_then(|v| v.as_object());
286288
let (input, cached, output, used_total) = if let Some(info) = info {
287-
if let Some(total) =
288-
find_usage_map(info, &["total_token_usage", "totalTokenUsage"])
289+
if let Some(total) = find_usage_map(info, &["total_token_usage", "totalTokenUsage"])
289290
{
290291
(
291292
read_i64(total, &["input_tokens", "inputTokens"]),
@@ -338,7 +339,11 @@ fn scan_file(
338339
cached: (cached - prev.cached).max(0),
339340
output: (output - prev.output).max(0),
340341
};
341-
previous_totals = Some(UsageTotals { input, cached, output });
342+
previous_totals = Some(UsageTotals {
343+
input,
344+
cached,
345+
output,
346+
});
342347
} else {
343348
// Some streams emit `last_token_usage` deltas between `total_token_usage` snapshots.
344349
// Treat those as already-counted to avoid double-counting when the next total arrives.
@@ -443,7 +448,11 @@ fn find_usage_map<'a>(
443448
fn read_i64(map: &serde_json::Map<String, Value>, keys: &[&str]) -> i64 {
444449
keys.iter()
445450
.find_map(|key| map.get(*key))
446-
.and_then(|value| value.as_i64().or_else(|| value.as_f64().map(|value| value as i64)))
451+
.and_then(|value| {
452+
value
453+
.as_i64()
454+
.or_else(|| value.as_f64().map(|value| value as i64))
455+
})
447456
.unwrap_or(0)
448457
}
449458

@@ -523,7 +532,9 @@ fn resolve_sessions_roots(
523532
if let Some(workspace_path) = workspace_path {
524533
let codex_home_override =
525534
resolve_workspace_codex_home_for_path(workspaces, Some(workspace_path));
526-
return resolve_codex_sessions_root(codex_home_override).into_iter().collect();
535+
return resolve_codex_sessions_root(codex_home_override)
536+
.into_iter()
537+
.collect();
527538
}
528539

529540
let mut roots = Vec::new();
@@ -607,10 +618,7 @@ mod tests {
607618

608619
fn make_temp_sessions_root() -> PathBuf {
609620
let mut root = std::env::temp_dir();
610-
root.push(format!(
611-
"codexmonitor-local-usage-root-{}",
612-
Uuid::new_v4()
613-
));
621+
root.push(format!("codexmonitor-local-usage-root-{}", Uuid::new_v4()));
614622
fs::create_dir_all(&root).expect("create temp root");
615623
root
616624
}
@@ -765,11 +773,9 @@ mod tests {
765773
.last()
766774
.cloned()
767775
.unwrap_or_else(|| Local::now().format("%Y-%m-%d").to_string());
768-
let naive = NaiveDateTime::parse_from_str(
769-
&format!("{day_key} 12:00:00"),
770-
"%Y-%m-%d %H:%M:%S",
771-
)
772-
.expect("timestamp");
776+
let naive =
777+
NaiveDateTime::parse_from_str(&format!("{day_key} 12:00:00"), "%Y-%m-%d %H:%M:%S")
778+
.expect("timestamp");
773779
let timestamp_ms = Local
774780
.from_local_datetime(&naive)
775781
.single()

src-tauri/src/shared/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@ pub(crate) mod local_usage_core;
88
pub(crate) mod process_core;
99
pub(crate) mod prompts_core;
1010
pub(crate) mod settings_core;
11-
pub(crate) mod worktree_core;
1211
pub(crate) mod workspaces_core;
12+
pub(crate) mod worktree_core;

0 commit comments

Comments
 (0)