fix: improve error handling across Rust and TypeScript

- Log swallowed errors in file watcher and window operations (lib.rs, watcher.rs)
- Propagate recovery errors from bridge::recover_pending to SyncResult.errors
  so the frontend can display them instead of silently dropping failures
- Fix useTauriEvent/useTauriEvents race condition where cleanup fires before
  async listen() resolves, leaking the listener (cancelled flag pattern)
- Guard computeStaleness against invalid date strings (NaN -> 'normal'
  instead of incorrectly returning 'urgent')
- Strengthen isMcError type guard to check field types, not just presence
- Log warning when data directory resolution falls back to '.' (state.rs, bridge.rs)
- Add test for computeStaleness with invalid date inputs
This commit is contained in:
teernisse
2026-02-26 10:13:17 -05:00
parent 29b44f1b4c
commit 23a4e6bf19
7 changed files with 556 additions and 13 deletions

View File

@@ -157,9 +157,14 @@ fn sync_now_inner(
let mut map = bridge.load_map()?;
// Recover any pending entries from a previous crash
bridge.recover_pending(&mut map)?;
let (_recovered, recovery_errors) = bridge.recover_pending(&mut map)?;
let result = bridge.incremental_sync(&mut map)?;
let mut result = bridge.incremental_sync(&mut map)?;
// Surface recovery errors alongside sync errors
if !recovery_errors.is_empty() {
result.errors.extend(recovery_errors);
}
Ok(result)
}
@@ -182,9 +187,14 @@ fn reconcile_inner(
let mut map = bridge.load_map()?;
// Recover pending first
bridge.recover_pending(&mut map)?;
let (_recovered, recovery_errors) = bridge.recover_pending(&mut map)?;
let result = bridge.full_reconciliation(&mut map)?;
let mut result = bridge.full_reconciliation(&mut map)?;
// Surface recovery errors alongside reconciliation errors
if !recovery_errors.is_empty() {
result.errors.extend(recovery_errors);
}
Ok(result)
}

View File

@@ -152,7 +152,12 @@ impl<L: LoreCli, B: BeadsCli> Bridge<L, B> {
/// Create a new bridge with the given CLI implementations
pub fn new(lore: L, beads: B) -> Self {
let data_dir = dirs::data_local_dir()
.unwrap_or_else(|| PathBuf::from("."))
.unwrap_or_else(|| {
tracing::warn!(
"Could not determine local data directory ($HOME may be unset), falling back to '.'"
);
PathBuf::from(".")
})
.join("mc");
Self {
@@ -331,7 +336,9 @@ impl<L: LoreCli, B: BeadsCli> Bridge<L, B> {
/// On startup, scan for entries with pending=true:
/// - If bead_id is None -> retry creation
/// - If bead_id exists -> clear pending flag
pub fn recover_pending(&self, map: &mut GitLabBeadMap) -> Result<usize, BridgeError> {
///
/// Returns (recovered_count, error_messages) so callers can surface failures.
pub fn recover_pending(&self, map: &mut GitLabBeadMap) -> Result<(usize, Vec<String>), BridgeError> {
let pending_keys: Vec<String> = map
.mappings
.iter()
@@ -340,6 +347,7 @@ impl<L: LoreCli, B: BeadsCli> Bridge<L, B> {
.collect();
let mut recovered = 0;
let mut errors = Vec::new();
for key in &pending_keys {
if let Some(entry) = map.mappings.get_mut(key) {
@@ -356,6 +364,7 @@ impl<L: LoreCli, B: BeadsCli> Bridge<L, B> {
}
Err(e) => {
tracing::error!("Failed to recover bead for {}: {}", key, e);
errors.push(format!("Failed to recover pending bead for {}: {}", key, e));
}
}
} else {
@@ -369,7 +378,7 @@ impl<L: LoreCli, B: BeadsCli> Bridge<L, B> {
self.save_map(map)?;
}
Ok(recovered)
Ok((recovered, errors))
}
/// Incremental sync: process `since_last_check` events from lore.
@@ -904,9 +913,10 @@ mod tests {
);
bridge.save_map(&map).unwrap();
let recovered = bridge.recover_pending(&mut map).unwrap();
let (recovered, errors) = bridge.recover_pending(&mut map).unwrap();
assert_eq!(recovered, 1);
assert!(errors.is_empty());
let entry = &map.mappings["issue:g/p:42"];
assert_eq!(entry.bead_id, Some("bd-recovered".to_string()));
assert!(!entry.pending);
@@ -931,9 +941,10 @@ mod tests {
);
bridge.save_map(&map).unwrap();
let recovered = bridge.recover_pending(&mut map).unwrap();
let (recovered, errors) = bridge.recover_pending(&mut map).unwrap();
assert_eq!(recovered, 1);
assert!(errors.is_empty());
assert!(!map.mappings["issue:g/p:42"].pending);
}

View File

@@ -15,7 +15,12 @@ use std::path::PathBuf;
/// Get the Mission Control data directory
pub fn mc_data_dir() -> PathBuf {
dirs::data_local_dir()
.unwrap_or_else(|| PathBuf::from("."))
.unwrap_or_else(|| {
tracing::warn!(
"Could not determine local data directory ($HOME may be unset), falling back to '.'"
);
PathBuf::from(".")
})
.join("mc")
}