diff --git a/shared/packages/api/src/__tests__/filePath.spec.ts b/shared/packages/api/src/__tests__/filePath.spec.ts index 9d7c707b..9bd84692 100644 --- a/shared/packages/api/src/__tests__/filePath.spec.ts +++ b/shared/packages/api/src/__tests__/filePath.spec.ts @@ -47,6 +47,26 @@ describe('resolveFileWithoutExtension', () => { }) }) + test('returns found when filename has no extension', async () => { + touch('myclip') + const result = await resolveFileWithoutExtension(path.join(tmpDir, 'myclip')) + expect(result).toEqual({ + result: 'found', + fullPath: path.join(tmpDir, 'myclip'), + extension: '', + }) + }) + + test('returns found when path already includes the extension', async () => { + touch('myclip.mp4') + const result = await resolveFileWithoutExtension(path.join(tmpDir, 'myclip.mp4')) + expect(result).toEqual({ + result: 'found', + fullPath: path.join(tmpDir, 'myclip.mp4'), + extension: '', + }) + }) + test('returns multiple when more than one file matches', async () => { touch('myclip.mp4') touch('myclip.mov') @@ -59,6 +79,18 @@ describe('resolveFileWithoutExtension', () => { ) }) + test('returns multiple when both extensionless and extension files exist', async () => { + touch('myclip') + touch('myclip.mp4') + const result = await resolveFileWithoutExtension(path.join(tmpDir, 'myclip')) + expect(result.result).toBe('multiple') + if (result.result !== 'multiple') return + expect(result.matches).toHaveLength(2) + expect(result.matches).toEqual( + expect.arrayContaining([path.join(tmpDir, 'myclip'), path.join(tmpDir, 'myclip.mp4')]) + ) + }) + test('returns notFound when no files match', async () => { touch('other.mp4') const result = await resolveFileWithoutExtension(path.join(tmpDir, 'myclip')) diff --git a/shared/packages/api/src/filePath.ts b/shared/packages/api/src/filePath.ts index 9d5d6aa2..6307853a 100644 --- a/shared/packages/api/src/filePath.ts +++ b/shared/packages/api/src/filePath.ts @@ -38,7 +38,8 @@ export type FileResolutionResult = /** * Attempts to resolve a file path by matching filenames without extensions. * If the exact path doesn't exist, searches for files that start with the base name - * followed by a dot and any extension. + * followed by a dot and any extension. Exact basename matches (extensionless file, + * or input path that already includes the extension) return `extension: ''`. * * @param fullPath - The full path to the file to resolve * @returns A FileResolutionResult indicating whether the file was found, not found, had multiple matches, or encountered an error @@ -57,6 +58,18 @@ export type FileResolutionResult = * // If both file.mp4 and file.mov exist: * const result = await resolveFileWithoutExtension('/path/to/file') * // Returns: { result: 'multiple', matches: ['/path/to/file.mp4', '/path/to/file.mov'] } + * + * @example + * // If looking for /path/to/file and file (no extension) exists: + * // Returns: { result: 'found', fullPath: '/path/to/file', extension: '' } + * + * @example + * // If looking for /path/to/file.mp4 and file.mp4 exists: + * // Returns: { result: 'found', fullPath: '/path/to/file.mp4', extension: '' } + * + * @example + * // If both file and file.mp4 exist: + * // Returns: { result: 'multiple', matches: ['/path/to/file', '/path/to/file.mp4'] } */ export async function resolveFileWithoutExtension(fullPath: string): Promise { const dir = path.dirname(fullPath) @@ -64,7 +77,7 @@ export async function resolveFileWithoutExtension(fullPath: string): Promise f.startsWith(base + '.')) + const matches = files.filter((f) => f.startsWith(base + '.') || f === base) if (matches.length === 0) { return { result: 'notFound' }