From 51356b41954c4f70e856edace64ca006aae72d9a Mon Sep 17 00:00:00 2001 From: Isaac Sanchez-Hawkins <266845420+isanchez404@users.noreply.github.com> Date: Tue, 12 May 2026 04:10:39 -0400 Subject: [PATCH] fix(projects): use fallback icon MIME (#1197) Co-authored-by: Isaac Sanchez --- .../lib/opencode/project-icon-routes.js | 4 +- .../lib/opencode/project-icon-routes.test.js | 107 ++++++++++++++++++ 2 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 packages/web/server/lib/opencode/project-icon-routes.test.js diff --git a/packages/web/server/lib/opencode/project-icon-routes.js b/packages/web/server/lib/opencode/project-icon-routes.js index 634ad972..5acdab6f 100644 --- a/packages/web/server/lib/opencode/project-icon-routes.js +++ b/packages/web/server/lib/opencode/project-icon-routes.js @@ -204,7 +204,9 @@ export const registerProjectIconRoutes = (app, dependencies) => { try { const data = await fsPromises.readFile(iconPath); const ext = path.extname(iconPath).slice(1).toLowerCase(); - const resolvedMime = metadataMime || projectIconExtensionToMime[ext] || 'application/octet-stream'; + const resolvedMime = iconPath === preferredPath && metadataMime + ? metadataMime + : projectIconExtensionToMime[ext] || 'application/octet-stream'; const contentType = resolvedMime === 'image/svg+xml' ? 'image/svg+xml; charset=utf-8' : resolvedMime; if (resolvedMime === 'image/svg+xml' && requestedThemeVariant) { diff --git a/packages/web/server/lib/opencode/project-icon-routes.test.js b/packages/web/server/lib/opencode/project-icon-routes.test.js new file mode 100644 index 00000000..8eb92db2 --- /dev/null +++ b/packages/web/server/lib/opencode/project-icon-routes.test.js @@ -0,0 +1,107 @@ +import { describe, expect, it, vi } from 'vitest'; +import crypto from 'crypto'; +import path from 'path'; + +import { registerProjectIconRoutes } from './project-icon-routes.js'; + +const createRouteRegistry = () => { + const routes = new Map(); + + return { + app: { + get(routePath, handler) { + routes.set(`GET ${routePath}`, handler); + }, + post(routePath, handler) { + routes.set(`POST ${routePath}`, handler); + }, + put(routePath, handler) { + routes.set(`PUT ${routePath}`, handler); + }, + delete(routePath, handler) { + routes.set(`DELETE ${routePath}`, handler); + }, + }, + getRoute(method, routePath) { + return routes.get(`${method} ${routePath}`); + }, + }; +}; + +const createMockResponse = () => { + const headers = new Map(); + let statusCode = 200; + let body = null; + + return { + setHeader(name, value) { + headers.set(name.toLowerCase(), value); + }, + getHeader(name) { + return headers.get(name.toLowerCase()); + }, + status(code) { + statusCode = code; + return this; + }, + json(payload) { + body = payload; + return this; + }, + send(payload) { + body = payload; + return this; + }, + get statusCode() { + return statusCode; + }, + get body() { + return body; + }, + }; +}; + +describe('project icon routes', () => { + it('uses fallback file extension MIME when metadata points to a missing icon', async () => { + const { app, getRoute } = createRouteRegistry(); + const jpgBytes = Buffer.from('jpg-bytes'); + const enoent = Object.assign(new Error('missing'), { code: 'ENOENT' }); + const fsPromises = { + readFile: vi.fn(async (iconPath) => { + if (iconPath.endsWith('.jpg')) { + return jpgBytes; + } + throw enoent; + }), + }; + + registerProjectIconRoutes(app, { + fsPromises, + path, + crypto, + openchamberDataDir: '/tmp/openchamber-test', + sanitizeProjects: (projects) => projects, + readSettingsFromDiskMigrated: async () => ({ + projects: [{ + id: 'proj-1', + path: '/repo', + iconImage: { mime: 'image/png', updatedAt: 1, source: 'custom' }, + }], + }), + persistSettings: async () => ({}), + createFsSearchRuntime: () => ({ searchFilesystemFiles: async () => [] }), + spawn: vi.fn(), + resolveGitBinaryForSpawn: vi.fn(), + }); + + const res = createMockResponse(); + await getRoute('GET', '/api/projects/:projectId/icon')({ + params: { projectId: 'proj-1' }, + query: {}, + }, res); + + expect(res.statusCode).toBe(200); + expect(res.getHeader('Content-Type')).toBe('image/jpeg'); + expect(res.body).toBe(jpgBytes); + }); +});