Improve reset local data guidance for locked Windows files (#1811)

Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
This commit is contained in:
Ruifeng Xue
2026-05-15 20:32:14 -07:00
committed by GitHub
co-authored by Steven Enamakel
parent 7659b40829
commit 159c0aa8fe
6 changed files with 115 additions and 10 deletions
+3 -2
View File
@@ -50,8 +50,9 @@ const SettingsHome = () => {
setError(null);
const currentUserId = snapshot.auth.userId ?? snapshot.currentUser?._id ?? null;
await clearAllAppData({ clearSession, userId: currentUserId }); // restarts the app
} catch (_error) {
setError(t('clearData.failed'));
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
setError(message || t('clearData.failed'));
} finally {
setIsLoading(false);
}
@@ -179,5 +179,35 @@ describe('SettingsHome', () => {
expect(args).toMatchObject({ userId: null });
expect(typeof args.clearSession).toBe('function');
});
it('surfaces the core error message when clearAllAppData fails (Windows file-lock guidance)', async () => {
const user = userEvent.setup();
mockClearAllAppData.mockRejectedValueOnce(
new Error(
'Failed to remove C:\\Users\\me\\.openhuman because it is locked by another OpenHuman window or process. Close all OpenHuman windows and try again.'
)
);
renderSettingsHome();
await user.click(screen.getByText('Clear App Data').closest('button')!);
const confirmButtons = screen.getAllByRole('button', { name: /Clear App Data/i });
await user.click(confirmButtons[confirmButtons.length - 1]);
expect(
await screen.findByText(/locked by another OpenHuman window or process/)
).toBeInTheDocument();
});
it('falls back to the translated message when the error has no message', async () => {
const user = userEvent.setup();
mockClearAllAppData.mockRejectedValueOnce(new Error(''));
renderSettingsHome();
await user.click(screen.getByText('Clear App Data').closest('button')!);
const confirmButtons = screen.getAllByRole('button', { name: /Clear App Data/i });
await user.click(confirmButtons[confirmButtons.length - 1]);
expect(await screen.findByText(/Failed to clear data and logout/)).toBeInTheDocument();
});
});
});
+3 -1
View File
@@ -34,7 +34,9 @@ const Welcome = () => {
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
log('clearAllAppData failed: %s', message);
setResetError('Could not clear app data. Please quit and reopen OpenHuman, then try again.');
setResetError(
message || 'Could not clear app data. Please quit and reopen OpenHuman, then try again.'
);
setIsClearingAppData(false);
}
};
+12 -1
View File
@@ -191,11 +191,22 @@ describe('Welcome — decryption-failure recovery action', () => {
fireEvent.click(screen.getByRole('button', { name: /Clear app data & restart/ }));
await waitFor(() => {
expect(screen.getByText(/Could not clear app data/)).toBeInTheDocument();
expect(screen.getByText(/reset blew up/)).toBeInTheDocument();
});
// Button re-enabled so the user can retry.
expect(screen.getByRole('button', { name: /Clear app data & restart/ })).not.toBeDisabled();
});
it('falls back to the generic message when the error has no message', async () => {
mockClearAllAppData.mockRejectedValueOnce(new Error(''));
renderWithProviders(<Welcome />);
fireEvent.click(screen.getByRole('button', { name: /Clear app data & restart/ }));
await waitFor(() => {
expect(screen.getByText(/Could not clear app data/)).toBeInTheDocument();
});
});
});
describe('Welcome — Select runtime button', () => {
+45 -6
View File
@@ -92,6 +92,42 @@ fn config_openhuman_dir(config: &Config) -> PathBuf {
.map_or_else(|| PathBuf::from("."), PathBuf::from)
}
fn is_windows_file_lock_error(error: &std::io::Error) -> bool {
cfg!(windows) && matches!(error.raw_os_error(), Some(32 | 33))
}
fn reset_local_data_remove_error(path: &Path, error: &std::io::Error) -> String {
if is_windows_file_lock_error(error) {
tracing::warn!(
path = %path.display(),
error = %error,
"[config] reset_local_data: Windows file lock blocked local data deletion"
);
return format!(
"Failed to remove {} because it is locked by another OpenHuman window or process. Close all OpenHuman windows and try again. ({error})",
path.display()
);
}
format!("Failed to remove {}: {error}", path.display())
}
fn reset_local_data_marker_remove_error(path: &Path, error: &std::io::Error) -> String {
if is_windows_file_lock_error(error) {
tracing::warn!(
marker = %path.display(),
error = %error,
"[config] reset_local_data: Windows file lock blocked active workspace marker deletion"
);
return format!(
"Failed to remove active workspace marker {} because it is locked by another OpenHuman window or process. Close all OpenHuman windows and try again. ({error})",
path.display()
);
}
format!("Failed to remove active workspace marker: {error}")
}
/// Internal helper to reset local data by removing specific directories and markers.
async fn reset_local_data_for_paths(
current_openhuman_dir: &Path,
@@ -108,9 +144,12 @@ async fn reset_local_data_for_paths(
let mut removed_paths = Vec::new();
if active_workspace_marker.exists() {
tokio::fs::remove_file(&active_workspace_marker)
.await
.map_err(|e| format!("Failed to remove active workspace marker: {e}"))?;
if let Err(error) = tokio::fs::remove_file(&active_workspace_marker).await {
return Err(reset_local_data_marker_remove_error(
&active_workspace_marker,
&error,
));
}
tracing::debug!(
marker = %active_workspace_marker.display(),
"[config] reset_local_data: removed active workspace marker"
@@ -127,9 +166,9 @@ async fn reset_local_data_for_paths(
continue;
}
tokio::fs::remove_dir_all(target_dir)
.await
.map_err(|e| format!("Failed to remove {}: {e}", target_dir.display()))?;
if let Err(error) = tokio::fs::remove_dir_all(target_dir).await {
return Err(reset_local_data_remove_error(target_dir, &error));
}
tracing::debug!(
dir = %target_dir.display(),
"[config] reset_local_data: removed directory"
+22
View File
@@ -112,6 +112,28 @@ fn config_openhuman_dir_returns_config_path_parent() {
assert_eq!(config_openhuman_dir(&cfg), PathBuf::from("/tmp/xyz"));
}
#[cfg(windows)]
#[test]
fn reset_local_data_remove_error_explains_windows_file_locks() {
let err = std::io::Error::from_raw_os_error(32);
let msg =
reset_local_data_remove_error(std::path::Path::new("C:\\Users\\me\\.openhuman"), &err);
assert!(msg.contains("locked by another OpenHuman window or process"));
assert!(msg.contains("Close all OpenHuman windows and try again"));
}
#[cfg(windows)]
#[test]
fn reset_local_data_remove_error_explains_windows_lock_violation() {
let err = std::io::Error::from_raw_os_error(33);
let msg =
reset_local_data_remove_error(std::path::Path::new("C:\\Users\\me\\.openhuman"), &err);
assert!(msg.contains("locked by another OpenHuman window or process"));
assert!(msg.contains("Close all OpenHuman windows and try again"));
}
// ── get_runtime_flags / set_browser_allow_all ─────────────────
#[test]