From 97fa455fd56d870581e7dce887ac397ab951c174 Mon Sep 17 00:00:00 2001 From: Tyler South Date: Sat, 22 Aug 2026 14:23:24 -0400 Subject: [PATCH 1/2] Stop two CI flakes blocking the merge queue Both have failed PRs today that had nothing to do with them. TrayPanel: afterEach calls vi.restoreAllMocks(), which resets every mock to the implementation it was created with - and for a bare vi.fn() that is "return undefined". LocaleProvider does listen(...).then(...), so a call landing outside the beforeEach-to-test window threw TypeError: Cannot read properties of undefined (reading 'then') and failed whichever test was on screen. It was always the star-ask test because that is the only one that renders the prompt. eventMocks.listen is now declared with a resolved-promise default, so a restore leaves it harmless; beforeEach still installs the listener-capturing version. secure_file: the delete-pending retry added for the Windows release race also sat on a directory planted at the sibling path, which Windows reports as access-denied too but which never clears. That spent the whole grace period before failing and broke the sub-second bound in an_unenforceable_lock_fails_closed_instead_of_writing_unserialized. The retry now skips a directory, the grace is 250ms rather than a second, and the test asserts against that constant instead of a bare literal. --- .../src/surfaces/TrayPanel.test.tsx | 10 +++++++++- rust/src/secure_file.rs | 16 +++++++++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx b/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx index 62861443..21df318f 100644 --- a/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx +++ b/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx @@ -36,7 +36,15 @@ 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(() => 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] From 383d3b15c6e0ee1d03a049b4adb0df28ff61b9a8 Mon Sep 17 00:00:00 2001 From: Tyler South Date: Sat, 22 Aug 2026 14:31:15 -0400 Subject: [PATCH 2/2] Match the listen default to the mocked signature tsc infers the mock's call signature from the implementation it is created with, so a zero-argument default rejected the two-argument mockImplementation in beforeEach. The build typechecks the test files, so this failed CI even though vitest was green. --- apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx b/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx index 21df318f..c53ee962 100644 --- a/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx +++ b/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx @@ -44,7 +44,10 @@ const eventMocks = vi.hoisted(() => ({ // (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(() => Promise.resolve(() => {})), + listen: vi.fn( + (_event: string, _handler: (event: { payload: unknown }) => void) => + Promise.resolve(() => {}), + ), listeners: new Map void>>(), }));