diff --git a/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx b/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx index 62861443..c53ee962 100644 --- a/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx +++ b/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx @@ -36,7 +36,18 @@ const tauriMocks = vi.hoisted(() => ({ })); const eventMocks = vi.hoisted(() => ({ - listen: vi.fn(), + // Declared with a working default, not a bare vi.fn(). `afterEach` calls + // vi.restoreAllMocks(), which resets each mock to the implementation it was + // created with — for a bare vi.fn() that is "return undefined". LocaleProvider + // does `listen(...).then(...)`, so any call that lands outside the + // beforeEach-to-test window threw "Cannot read properties of undefined + // (reading 'then')" and failed whichever test was on screen. Restoring to a + // resolved promise keeps that harmless; beforeEach still installs the + // listener-capturing version the tests drive. + listen: vi.fn( + (_event: string, _handler: (event: { payload: unknown }) => void) => + Promise.resolve(() => {}), + ), listeners: new Map void>>(), })); diff --git a/rust/src/secure_file.rs b/rust/src/secure_file.rs index 958b9146..bc50abfe 100644 --- a/rust/src/secure_file.rs +++ b/rust/src/secure_file.rs @@ -36,7 +36,7 @@ const STATE_LOCK_STALE: std::time::Duration = std::time::Duration::from_secs(120 /// /// Unix unlinks by name and never reports this state, so the retry is compiled /// in everywhere but only enabled where it can happen. -const DELETE_PENDING_GRACE: std::time::Duration = std::time::Duration::from_secs(1); +const DELETE_PENDING_GRACE: std::time::Duration = std::time::Duration::from_millis(250); const RETRIES_DELETE_PENDING: bool = cfg!(windows); /// Serialize read-modify-write transactions across Ceiling processes. @@ -257,8 +257,12 @@ impl StateWriteLock { if !RETRIES_DELETE_PENDING { return attempt; } + // A directory sitting on the sibling path is also access-denied on + // Windows, and unlike a closing handle it never clears. Retrying + // that only delays a failure the caller needs now. let denied = matches!(&attempt, LockAttempt::Failed(error) - if error.kind() == io::ErrorKind::PermissionDenied); + if error.kind() == io::ErrorKind::PermissionDenied) + && !exclusive.is_dir(); if !denied || std::time::Instant::now() >= deadline { return attempt; } @@ -2000,7 +2004,13 @@ mod tests { attempts, 1, "a failed fallback must not be retried as success" ); - assert!(started.elapsed() < std::time::Duration::from_secs(1)); + // A directory on the sibling path is access-denied on Windows too, but + // it never clears. The delete-pending retry must not sit on it: this + // used to spend the whole grace period before failing. + assert!( + started.elapsed() < DELETE_PENDING_GRACE, + "an unfixable fallback must fail without waiting out the retry grace" + ); } #[test]