fix(desktop): recover from macOS directory permission failures
This commit is contained in:
@@ -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' });
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user