fix(desktop): recover from macOS directory permission failures

This commit is contained in:
deatheros
2026-08-07 01:49:44 +03:00
parent 7e0e22f6e2
commit d8518bf053
30 changed files with 497 additions and 106 deletions
+28
View File
@@ -25,6 +25,34 @@ const urls: RuntimeUrlResolver = {
};
describe('createWebFilesAPI', () => {
it('preserves the directory permission failure contract', async () => {
const { createWebFilesAPI } = await import('./files');
const api = createWebFilesAPI({ urls, getDirectory: () => '/workspace' });
runtimeFetchMock.mockResolvedValueOnce(Response.json(
{ error: 'Access to directory denied', reason: 'os-permission' },
{ status: 403 },
));
const error = await api.listDirectory('/protected').catch((caught) => caught);
expect(error).toMatchObject({
name: 'FilesystemError',
reason: 'os-permission',
status: 403,
message: 'Access to directory denied',
});
});
it('rejects malformed successful directory listings', async () => {
const { createWebFilesAPI } = await import('./files');
const api = createWebFilesAPI({ urls, getDirectory: () => '/workspace' });
runtimeFetchMock.mockResolvedValueOnce(Response.json({ path: '/workspace' }));
await expect(api.listDirectory('/workspace')).rejects.toMatchObject({
reason: 'invalid-response',
});
});
it('uses per-call workspace directory for stat and read requests', async () => {
const { createWebFilesAPI } = await import('./files');
const api = createWebFilesAPI({ urls, getDirectory: () => '/stale-workspace' });
+21 -4
View File
@@ -4,6 +4,10 @@ import type {
FileSearchResult,
FilesAPI,
} from '@openchamber/ui/lib/api/types';
import {
FilesystemError,
parseFilesystemErrorReason,
} from '@openchamber/ui/lib/api/files-errors';
import { runtimeFetch } from '@openchamber/ui/lib/runtime-fetch';
const normalizePath = (path: string): string => path.replace(/\\/g, '/');
@@ -28,12 +32,16 @@ type WebDirectoryListResponse = {
};
const toDirectoryListResult = (fallbackDirectory: string, payload: WebDirectoryListResponse): DirectoryListResult => {
if (!payload || !Array.isArray(payload.entries)) {
throw new FilesystemError('Directory listing returned an invalid response', {
reason: 'invalid-response',
});
}
const directory = normalizePath(payload?.directory || payload?.path || fallbackDirectory);
const entries = Array.isArray(payload?.entries) ? payload.entries : [];
return {
directory,
entries: entries
entries: payload.entries
.filter((entry): entry is Required<Pick<WebDirectoryEntry, 'name' | 'path'>> & { isDirectory?: boolean } =>
Boolean(entry && typeof entry.name === 'string' && typeof entry.path === 'string')
)
@@ -67,8 +75,17 @@ export const createWebFilesAPI = ({ getDirectory }: WebFilesAPIOptions): FilesAP
});
if (!response.ok) {
const error = await response.json().catch(() => ({ error: response.statusText }));
throw new Error((error as { error?: string }).error || 'Failed to list directory');
const error = await response.json().catch(() => ({ error: response.statusText })) as {
error?: string;
reason?: unknown;
};
throw new FilesystemError(
error.error || 'Failed to list directory',
{
reason: parseFilesystemErrorReason(error.reason),
status: response.status,
},
);
}
const result = (await response.json()) as WebDirectoryListResponse;