From 926decd2f1cdba439b695504385ee787aa895d4b Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Wed, 21 Oct 2026 16:19:40 -0700 Subject: [PATCH] refactor(jira): group attachment modules with concrete owner imports (#31683) --- src/main/jira/adf-link-mapping.test.ts | 22 +++++--- .../content-url.ts} | 8 +-- .../discovery.ts} | 4 +- .../image-bytes.ts} | 16 +++--- .../image-cache-generation.test.ts} | 2 +- .../image-cache.test.ts} | 4 +- .../image-cache.ts} | 2 +- .../image-grants.ts} | 8 +-- .../image-protocol.ts} | 4 +- .../image-read.test.ts} | 18 +++---- .../image-read.ts} | 8 +-- .../image-references.test.ts} | 26 ++++++---- .../images.test.ts} | 51 ++++++++++--------- .../images.ts} | 23 +++++---- .../legacy-cancellation.test.ts} | 18 ++++--- .../meta.ts} | 0 src/main/jira/client-scoped-token.test.ts | 2 +- src/main/jira/client.ts | 2 +- src/main/jira/issues.test.ts | 18 ++++--- src/main/jira/jira-issue-comments.ts | 2 +- src/main/jira/jira-issue-media.ts | 4 +- .../rpc/methods/jira-attachment-image.test.ts | 6 +-- .../rpc/methods/jira-attachment-image.ts | 4 +- src/main/startup/main-process-preflight.ts | 2 +- .../startup/main-process-ready-foundation.ts | 2 +- 25 files changed, 143 insertions(+), 113 deletions(-) rename src/main/jira/{attachment-content-url.ts => attachments/content-url.ts} (78%) rename src/main/jira/{attachment-discovery.ts => attachments/discovery.ts} (95%) rename src/main/jira/{attachment-image-bytes.ts => attachments/image-bytes.ts} (84%) rename src/main/jira/{attachment-image-cache-generation.test.ts => attachments/image-cache-generation.test.ts} (98%) rename src/main/jira/{attachment-image-cache.test.ts => attachments/image-cache.test.ts} (98%) rename src/main/jira/{attachment-image-cache.ts => attachments/image-cache.ts} (99%) rename src/main/jira/{attachment-image-grants.ts => attachments/image-grants.ts} (88%) rename src/main/jira/{attachment-image-protocol.ts => attachments/image-protocol.ts} (87%) rename src/main/jira/{attachment-image-read.test.ts => attachments/image-read.test.ts} (94%) rename src/main/jira/{attachment-image-read.ts => attachments/image-read.ts} (74%) rename src/main/jira/{attachment-image-references.test.ts => attachments/image-references.test.ts} (83%) rename src/main/jira/{attachment-images.test.ts => attachments/images.test.ts} (90%) rename src/main/jira/{attachment-images.ts => attachments/images.ts} (92%) rename src/main/jira/{attachment-legacy-cancellation.test.ts => attachments/legacy-cancellation.test.ts} (87%) rename src/main/jira/{attachment-meta.ts => attachments/meta.ts} (100%) diff --git a/src/main/jira/adf-link-mapping.test.ts b/src/main/jira/adf-link-mapping.test.ts index e13c36e0a05..62f2b046bd5 100644 --- a/src/main/jira/adf-link-mapping.test.ts +++ b/src/main/jira/adf-link-mapping.test.ts @@ -21,6 +21,18 @@ const { vi.mock('./request-queue', () => ({ acquire: acquireMock, release: releaseMock })) +const { JiraApiError: JiraApiErrorMock } = vi.hoisted(() => ({ + JiraApiError: class JiraApiError extends Error { + status: number | null + constructor(message: string, status: number | null = null) { + super(message) + this.status = status + } + } +})) + +vi.mock('./jira-api-error', () => ({ JiraApiError: JiraApiErrorMock })) + vi.mock('./authenticated-request', () => ({ apiBasePath: (site: { authType?: string }) => site.authType === 'server' ? '/rest/api/2' : '/rest/api/3', @@ -29,13 +41,7 @@ vi.mock('./authenticated-request', () => ({ isAuthError: (...args: unknown[]) => isAuthErrorMock(...args), jiraRequest: (...args: unknown[]) => jiraRequestMock(...args), jiraRequestBinary: (...args: unknown[]) => jiraRequestBinaryMock(...args), - JiraApiError: class JiraApiError extends Error { - status: number | null - constructor(message: string, status: number | null = null) { - super(message) - this.status = status - } - } + JiraApiError: JiraApiErrorMock })) vi.mock('./client', () => ({ @@ -80,7 +86,7 @@ describe('Jira ADF link mark mapping', () => { releaseMock.mockImplementation(() => {}) jiraRequestBinaryMock.mockReset() jiraRequestMock.mockReset() - const { _resetAttachmentImageCache } = await import('./attachment-image-cache') + const { _resetAttachmentImageCache } = await import('./attachments/image-cache') _resetAttachmentImageCache() }) diff --git a/src/main/jira/attachment-content-url.ts b/src/main/jira/attachments/content-url.ts similarity index 78% rename from src/main/jira/attachment-content-url.ts rename to src/main/jira/attachments/content-url.ts index 102f8cf8b89..1840f7ab588 100644 --- a/src/main/jira/attachment-content-url.ts +++ b/src/main/jira/attachments/content-url.ts @@ -1,7 +1,7 @@ -import type { JiraSite } from '../../shared/jira-types' -import { apiBasePath } from './authenticated-request' -import { isJiraGatewayBaseUrl } from './gateway-base-url' -import type { AttachmentMeta } from './attachment-meta' +import type { JiraSite } from '../../../shared/jira-types' +import { apiBasePath } from '../authenticated-request' +import { isJiraGatewayBaseUrl } from '../gateway-base-url' +import type { AttachmentMeta } from './meta' export function attachmentContentUrl(site: JiraSite, meta: AttachmentMeta): URL | null { if (!meta.contentUrl && site.authType === 'server') { diff --git a/src/main/jira/attachment-discovery.ts b/src/main/jira/attachments/discovery.ts similarity index 95% rename from src/main/jira/attachment-discovery.ts rename to src/main/jira/attachments/discovery.ts index e16dd73e899..96b34efa87b 100644 --- a/src/main/jira/attachment-discovery.ts +++ b/src/main/jira/attachments/discovery.ts @@ -1,5 +1,5 @@ -import type { JiraAdfMediaAttrs } from './adf-markdown' -import { MAX_IMAGES, parseImageAttachmentMetas } from './attachment-meta' +import type { JiraAdfMediaAttrs } from '../adf-markdown' +import { MAX_IMAGES, parseImageAttachmentMetas } from './meta' /** * Pull attachment content IDs from Jira rendered HTML in document order. diff --git a/src/main/jira/attachment-image-bytes.ts b/src/main/jira/attachments/image-bytes.ts similarity index 84% rename from src/main/jira/attachment-image-bytes.ts rename to src/main/jira/attachments/image-bytes.ts index 7ff51044c46..fa41abcb6ed 100644 --- a/src/main/jira/attachment-image-bytes.ts +++ b/src/main/jira/attachments/image-bytes.ts @@ -1,20 +1,20 @@ -import { jiraRequestBinary, type JiraClientForSite } from './authenticated-request' -import type { AttachmentMeta } from './attachment-meta' -import { attachmentContentUrl } from './attachment-content-url' +import { jiraRequestBinary, type JiraClientForSite } from '../authenticated-request' +import type { AttachmentMeta } from './meta' +import { attachmentContentUrl } from './content-url' import { loadAttachmentBytesWithCache, type AttachmentImageBytes, attachmentImagesEpoch, getCachedAttachmentBytes -} from './attachment-image-cache' +} from './image-cache' import { JIRA_IMAGE_MAX_BYTES, JIRA_IMAGE_MAX_PENDING_READS, isJiraImageMimeType -} from '../../shared/jira-attachment-image' -import { withJiraDeadline } from './jira-read-failure' -import { PrioritySemaphore } from '../../shared/priority-semaphore' -import { waitForPromiseWithSignal } from '../../shared/abort-signal-reason' +} from '../../../shared/jira-attachment-image' +import { withJiraDeadline } from '../jira-read-failure' +import { PrioritySemaphore } from '../../../shared/priority-semaphore' +import { waitForPromiseWithSignal } from '../../../shared/abort-signal-reason' const downloads = new PrioritySemaphore(3) const interests = new Map() diff --git a/src/main/jira/attachment-image-cache-generation.test.ts b/src/main/jira/attachments/image-cache-generation.test.ts similarity index 98% rename from src/main/jira/attachment-image-cache-generation.test.ts rename to src/main/jira/attachments/image-cache-generation.test.ts index e92ca5c5a53..e30e72a15b5 100644 --- a/src/main/jira/attachment-image-cache-generation.test.ts +++ b/src/main/jira/attachments/image-cache-generation.test.ts @@ -4,7 +4,7 @@ import { clearAttachmentImagesForSite, getCachedAttachmentDataUrl, loadAttachmentDataUrlWithCache -} from './attachment-image-cache' +} from './image-cache' type Image = { dataUrl: string; byteSize: number } | null function deferredImage() { diff --git a/src/main/jira/attachment-image-cache.test.ts b/src/main/jira/attachments/image-cache.test.ts similarity index 98% rename from src/main/jira/attachment-image-cache.test.ts rename to src/main/jira/attachments/image-cache.test.ts index 5e8087995cb..3c30de39f57 100644 --- a/src/main/jira/attachment-image-cache.test.ts +++ b/src/main/jira/attachments/image-cache.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { MAX_TIMER_DELAY_MS } from '../../shared/timer-delay' +import { MAX_TIMER_DELAY_MS } from '../../../shared/timer-delay' import { _getAttachmentImageCacheSize, _resetAttachmentImageCache, @@ -7,7 +7,7 @@ import { getCachedAttachmentDataUrl, loadAttachmentDataUrlWithCache, setCachedAttachmentDataUrl -} from './attachment-image-cache' +} from './image-cache' describe('attachment image cache', () => { beforeEach(() => { diff --git a/src/main/jira/attachment-image-cache.ts b/src/main/jira/attachments/image-cache.ts similarity index 99% rename from src/main/jira/attachment-image-cache.ts rename to src/main/jira/attachments/image-cache.ts index f520901ef97..1552671b8a0 100644 --- a/src/main/jira/attachment-image-cache.ts +++ b/src/main/jira/attachments/image-cache.ts @@ -1,6 +1,6 @@ // Description, comments, and lazy image reads share one bounded cache. -import { MAX_TIMER_DELAY_MS } from '../../shared/timer-delay' +import { MAX_TIMER_DELAY_MS } from '../../../shared/timer-delay' const CACHE_TTL_MS = 30 * 60_000 const MAX_CACHE_ENTRIES = 96 diff --git a/src/main/jira/attachment-image-grants.ts b/src/main/jira/attachments/image-grants.ts similarity index 88% rename from src/main/jira/attachment-image-grants.ts rename to src/main/jira/attachments/image-grants.ts index 80959907f4e..76f27d612e9 100644 --- a/src/main/jira/attachment-image-grants.ts +++ b/src/main/jira/attachments/image-grants.ts @@ -1,8 +1,8 @@ import { randomUUID } from 'node:crypto' -import { JIRA_IMAGE_SCHEME } from '../../shared/jira-attachment-image' -import { attachmentImagesEpoch } from './attachment-image-cache' -import type { JiraClientForSite } from './authenticated-request' -import type { AttachmentMeta } from './attachment-meta' +import { JIRA_IMAGE_SCHEME } from '../../../shared/jira-attachment-image' +import { attachmentImagesEpoch } from './image-cache' +import type { JiraClientForSite } from '../authenticated-request' +import type { AttachmentMeta } from './meta' const GRANT_TTL_MS = 30 * 60_000 const MAX_GRANTS = 2048 diff --git a/src/main/jira/attachment-image-protocol.ts b/src/main/jira/attachments/image-protocol.ts similarity index 87% rename from src/main/jira/attachment-image-protocol.ts rename to src/main/jira/attachments/image-protocol.ts index df5c6ebcad1..eead810e3d0 100644 --- a/src/main/jira/attachment-image-protocol.ts +++ b/src/main/jira/attachments/image-protocol.ts @@ -1,6 +1,6 @@ import { protocol } from 'electron' -import { JIRA_IMAGE_SCHEME, jiraImageReference } from '../../shared/jira-attachment-image' -import { readGrantedAttachmentImage } from './attachment-image-read' +import { JIRA_IMAGE_SCHEME, jiraImageReference } from '../../../shared/jira-attachment-image' +import { readGrantedAttachmentImage } from './image-read' export const JIRA_IMAGE_CUSTOM_SCHEME: Electron.CustomScheme = { scheme: JIRA_IMAGE_SCHEME, diff --git a/src/main/jira/attachment-image-read.test.ts b/src/main/jira/attachments/image-read.test.ts similarity index 94% rename from src/main/jira/attachment-image-read.test.ts rename to src/main/jira/attachments/image-read.test.ts index 2c86798413b..b5902e673eb 100644 --- a/src/main/jira/attachment-image-read.test.ts +++ b/src/main/jira/attachments/image-read.test.ts @@ -1,22 +1,22 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { JiraClientForSite } from './authenticated-request' -import { JIRA_IMAGE_MAX_PENDING_READS } from '../../shared/jira-attachment-image' -import { readAttachmentImageBytes } from './attachment-image-bytes' +import type { JiraClientForSite } from '../authenticated-request' +import { JIRA_IMAGE_MAX_PENDING_READS } from '../../../shared/jira-attachment-image' +import { readAttachmentImageBytes } from './image-bytes' import { registerAttachmentImageGrant, attachmentImageGrant, resetAttachmentImageGrantsForTest -} from './attachment-image-grants' -import { readGrantedAttachmentImage } from './attachment-image-read' -import { handleJiraImageRequest } from './attachment-image-protocol' -import { clearAttachmentImagesForSite, _resetAttachmentImageCache } from './attachment-image-cache' +} from './image-grants' +import { readGrantedAttachmentImage } from './image-read' +import { handleJiraImageRequest } from './image-protocol' +import { clearAttachmentImagesForSite, _resetAttachmentImageCache } from './image-cache' const { binary, clients } = vi.hoisted(() => ({ binary: vi.fn(), clients: vi.fn() })) -vi.mock('./authenticated-request', () => ({ +vi.mock('../authenticated-request', () => ({ apiBasePath: () => '/rest/api/3', jiraRequestBinary: binary })) -vi.mock('./client', () => ({ getClients: clients })) +vi.mock('../client', () => ({ getClients: clients })) vi.mock('electron', () => ({ protocol: { handle: vi.fn() } })) const client: JiraClientForSite = { site: { id: 'site', siteUrl: 'https://jira.test', email: '', accountId: '', displayName: '' }, diff --git a/src/main/jira/attachment-image-read.ts b/src/main/jira/attachments/image-read.ts similarity index 74% rename from src/main/jira/attachment-image-read.ts rename to src/main/jira/attachments/image-read.ts index 914b09648ce..00bc9e1bbaf 100644 --- a/src/main/jira/attachment-image-read.ts +++ b/src/main/jira/attachments/image-read.ts @@ -1,7 +1,7 @@ -import { getClients } from './client' -import { attachmentImageGrant, isAttachmentImageGrantCurrent } from './attachment-image-grants' -import { readAttachmentImageBytes } from './attachment-image-bytes' -import type { AttachmentImageBytes } from './attachment-image-cache' +import { getClients } from '../client' +import { attachmentImageGrant, isAttachmentImageGrantCurrent } from './image-grants' +import { readAttachmentImageBytes } from './image-bytes' +import type { AttachmentImageBytes } from './image-cache' export async function readGrantedAttachmentImage( reference: string, diff --git a/src/main/jira/attachment-image-references.test.ts b/src/main/jira/attachments/image-references.test.ts similarity index 83% rename from src/main/jira/attachment-image-references.test.ts rename to src/main/jira/attachments/image-references.test.ts index ed063f549d4..34364a80baf 100644 --- a/src/main/jira/attachment-image-references.test.ts +++ b/src/main/jira/attachments/image-references.test.ts @@ -1,14 +1,20 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import { JIRA_IMAGE_MAX_BYTES } from '../../shared/jira-attachment-image' -import { loadIssueImageAttachments, createMediaMarkdownResolver } from './attachment-images' -import { attachmentImageGrant, resetAttachmentImageGrantsForTest } from './attachment-image-grants' -import { clearAttachmentImagesForSite, _resetAttachmentImageCache } from './attachment-image-cache' -import type { JiraClientForSite } from './authenticated-request' +import { JIRA_IMAGE_MAX_BYTES } from '../../../shared/jira-attachment-image' +import { loadIssueImageAttachments, createMediaMarkdownResolver } from './images' +import { attachmentImageGrant, resetAttachmentImageGrantsForTest } from './image-grants' +import { clearAttachmentImagesForSite, _resetAttachmentImageCache } from './image-cache' +import type { JiraClientForSite } from '../authenticated-request' -vi.mock('./authenticated-request', () => ({ +const { JiraApiError: JiraApiErrorMock } = vi.hoisted(() => ({ + JiraApiError: class extends Error {} +})) + +vi.mock('../jira-api-error', () => ({ JiraApiError: JiraApiErrorMock })) + +vi.mock('../authenticated-request', () => ({ apiBasePath: () => '/rest/api/3', jiraRequestBinary: vi.fn(), - JiraApiError: class extends Error {} + JiraApiError: JiraApiErrorMock })) const client: JiraClientForSite = { site: { @@ -37,7 +43,7 @@ describe('lazy Jira image references', () => { vi.clearAllMocks() }) it('keeps image bytes out of Markdown without downloading the attachment', async () => { - const { jiraRequestBinary } = await import('./authenticated-request') + const { jiraRequestBinary } = await import('../authenticated-request') const images = await loadIssueImageAttachments(client, metadata, ['1'], 'reference') const markdown = createMediaMarkdownResolver(images, ['1'])({ id: 'media-id', alt: 'shot.png' }) expect(markdown).toMatch(/^!\[shot.png\]\(orca-jira-image:\/\/attachment\/[0-9a-f-]+\)$/) @@ -64,7 +70,7 @@ describe('lazy Jira image references', () => { }) it('discovers larger images by filename when rendered HTML has no attachment IDs', async () => { - const { collectIssueMediaRequest } = await import('./jira-issue-media') + const { collectIssueMediaRequest } = await import('../jira-issue-media') const raw = { key: 'JIRA-1', fields: { @@ -85,7 +91,7 @@ it('discovers larger images by filename when rendered HTML has no attachment IDs }) it('keeps the old per-image cap when an old client reuses a newer byte-cache entry', async () => { - const { loadAttachmentBytesWithCache } = await import('./attachment-image-cache') + const { loadAttachmentBytesWithCache } = await import('./image-cache') _resetAttachmentImageCache() await loadAttachmentBytesWithCache({ siteId: client.site.id, diff --git a/src/main/jira/attachment-images.test.ts b/src/main/jira/attachments/images.test.ts similarity index 90% rename from src/main/jira/attachment-images.test.ts rename to src/main/jira/attachments/images.test.ts index efd3b8b42ec..c139d873feb 100644 --- a/src/main/jira/attachment-images.test.ts +++ b/src/main/jira/attachments/images.test.ts @@ -1,14 +1,11 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { JiraClientForSite } from './authenticated-request' +import type { JiraClientForSite } from '../authenticated-request' const { jiraRequestBinaryMock } = vi.hoisted(() => ({ jiraRequestBinaryMock: vi.fn() })) -vi.mock('./authenticated-request', () => ({ - jiraRequestBinary: (...args: unknown[]) => jiraRequestBinaryMock(...args), - apiBasePath: (site: { authType?: string }) => - site.authType === 'server' ? '/rest/api/2' : '/rest/api/3', +const { JiraApiError: JiraApiErrorMock } = vi.hoisted(() => ({ JiraApiError: class JiraApiError extends Error { status: number | null constructor(message: string, status: number | null = null) { @@ -18,6 +15,15 @@ vi.mock('./authenticated-request', () => ({ } })) +vi.mock('../jira-api-error', () => ({ JiraApiError: JiraApiErrorMock })) + +vi.mock('../authenticated-request', () => ({ + jiraRequestBinary: (...args: unknown[]) => jiraRequestBinaryMock(...args), + apiBasePath: (site: { authType?: string }) => + site.authType === 'server' ? '/rest/api/2' : '/rest/api/3', + JiraApiError: JiraApiErrorMock +})) + function makeEntry(): JiraClientForSite { return { site: { @@ -34,12 +40,12 @@ function makeEntry(): JiraClientForSite { describe('attachment image helpers', () => { beforeEach(async () => { jiraRequestBinaryMock.mockReset() - const { _resetAttachmentImageCache } = await import('./attachment-image-cache') + const { _resetAttachmentImageCache } = await import('./image-cache') _resetAttachmentImageCache() }) it('extracts attachment content ids from rendered HTML in order', async () => { - const { extractAttachmentContentIdsFromHtml } = await import('./attachment-discovery') + const { extractAttachmentContentIdsFromHtml } = await import('./discovery') const ids = extractAttachmentContentIdsFromHtml(`

intro

@@ -50,7 +56,7 @@ describe('attachment image helpers', () => { }) it('extracts thumbnail attachment ids', async () => { - const { extractAttachmentContentIdsFromHtml } = await import('./attachment-discovery') + const { extractAttachmentContentIdsFromHtml } = await import('./discovery') expect( extractAttachmentContentIdsFromHtml( '' @@ -70,8 +76,7 @@ describe('attachment image helpers', () => { contentType: 'image/png' }) - const { createMediaMarkdownResolver, loadIssueImageAttachments } = - await import('./attachment-images') + const { createMediaMarkdownResolver, loadIssueImageAttachments } = await import('./images') const images = await loadIssueImageAttachments( makeEntry(), @@ -108,7 +113,7 @@ describe('attachment image helpers', () => { }) it('pairs shared alt filenames to distinct attachments then falls through', async () => { - const { createMediaMarkdownResolver } = await import('./attachment-images') + const { createMediaMarkdownResolver } = await import('./images') const images = [ { id: '1', @@ -132,7 +137,7 @@ describe('attachment image helpers', () => { }) it('does not re-emit an already consumed attachment for a third shared-alt node', async () => { - const { createMediaMarkdownResolver } = await import('./attachment-images') + const { createMediaMarkdownResolver } = await import('./images') const images = [ { id: '1', @@ -156,7 +161,7 @@ describe('attachment image helpers', () => { }) it('escapes hostile external media URLs instead of injecting markdown', async () => { - const { createMediaMarkdownResolver } = await import('./attachment-images') + const { createMediaMarkdownResolver } = await import('./images') const resolve = createMediaMarkdownResolver([], []) const hostile = 'https://evil.example/x?a=1)![z](javascript:alert(1))' const out = resolve({ url: hostile, alt: 'Image' }) @@ -168,14 +173,14 @@ describe('attachment image helpers', () => { }) it('returns placeholder for external URLs that remain hostile after encode', async () => { - const { createMediaMarkdownResolver } = await import('./attachment-images') + const { createMediaMarkdownResolver } = await import('./images') const resolve = createMediaMarkdownResolver([], []) // non-http rejected expect(resolve({ url: 'javascript:alert(1)', alt: 'x' })).toBe('*[x]*') }) it('selects preferred ids via Option A filename fallback without sweeping all attachments', async () => { - const { selectPreferredAttachmentIds } = await import('./attachment-discovery') + const { selectPreferredAttachmentIds } = await import('./discovery') const attachments = [ { id: '1', filename: 'a.png', mimeType: 'image/png', size: 1 }, { id: '2', filename: 'b.png', mimeType: 'image/png', size: 1 }, @@ -200,7 +205,7 @@ describe('attachment image helpers', () => { }) it('Option A unions multiple same-filename attachments for repeated alts', async () => { - const { selectPreferredAttachmentIds } = await import('./attachment-discovery') + const { selectPreferredAttachmentIds } = await import('./discovery') const attachments = [ { id: '1', filename: 'image.png', mimeType: 'image/png', size: 1 }, { id: '2', filename: 'image.png', mimeType: 'image/png', size: 1 } @@ -226,7 +231,7 @@ describe('attachment image helpers', () => { data: Uint8Array.from([1]).buffer, contentType: 'image/png' }) - const { loadIssueImageAttachments } = await import('./attachment-images') + const { loadIssueImageAttachments } = await import('./images') const attachments = Array.from({ length: 13 }, (_, index) => ({ id: String(index + 1), filename: `${index + 1}.png`, @@ -245,7 +250,7 @@ describe('attachment image helpers', () => { }) it('does not download attachments when rendered content references none', async () => { - const { loadIssueImageAttachments } = await import('./attachment-images') + const { loadIssueImageAttachments } = await import('./images') await expect( loadIssueImageAttachments( @@ -268,7 +273,7 @@ describe('attachment image helpers', () => { siteUrl: 'https://jira.example.com/jira', authType: 'server' } - const { loadIssueImageAttachments } = await import('./attachment-images') + const { loadIssueImageAttachments } = await import('./images') await loadIssueImageAttachments( entry, @@ -291,7 +296,7 @@ describe('attachment image helpers', () => { }) it('skips oversized and non-image attachments', async () => { - const { parseImageAttachmentMetas } = await import('./attachment-meta') + const { parseImageAttachmentMetas } = await import('./meta') expect( parseImageAttachmentMetas([ { id: '1', filename: 'big.png', mimeType: 'image/png', size: 20 * 1024 * 1024 }, @@ -306,7 +311,7 @@ describe('attachment image helpers', () => { data: Uint8Array.from([1, 2, 3]).buffer, contentType: 'image/png' }) - const { loadIssueImageAttachments } = await import('./attachment-images') + const { loadIssueImageAttachments } = await import('./images') const entry = makeEntry() const field = [{ id: '9', filename: 'c.png', mimeType: 'image/png', size: 3 }] await loadIssueImageAttachments(entry, field, ['9']) @@ -322,7 +327,7 @@ describe('attachment image helpers', () => { resolveDownload = resolve }) ) - const { loadIssueImageAttachments } = await import('./attachment-images') + const { loadIssueImageAttachments } = await import('./images') const entry = makeEntry() const field = [{ id: '7', filename: 's.png', mimeType: 'image/png', size: 1 }] const p1 = loadIssueImageAttachments(entry, field, ['7']) @@ -336,7 +341,7 @@ describe('attachment image helpers', () => { it('warns when media resolution is incomplete', async () => { const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) - const { warnIfMediaResolutionIncomplete } = await import('./attachment-discovery') + const { warnIfMediaResolutionIncomplete } = await import('./discovery') warnIfMediaResolutionIncomplete({ siteId: 's', issueKey: 'ABC-1', diff --git a/src/main/jira/attachment-images.ts b/src/main/jira/attachments/images.ts similarity index 92% rename from src/main/jira/attachment-images.ts rename to src/main/jira/attachments/images.ts index 61b8a1cca6b..2a45e89ce63 100644 --- a/src/main/jira/attachment-images.ts +++ b/src/main/jira/attachments/images.ts @@ -1,10 +1,11 @@ -import type { JiraClientForSite } from './authenticated-request' -import { attachmentContentUrl } from './attachment-content-url' -import { JiraApiError, jiraRequestBinary } from './authenticated-request' -import type { JiraAdfMediaAttrs, JiraAdfMediaResolver } from './adf-markdown' -import { escapeMarkdownAlt, unresolvedMediaPlaceholder } from './adf-markdown' -import { escapeMarkdownLinkDestination } from './adf-media-destination' -import { loadAttachmentDataUrlWithCache } from './attachment-image-cache' +import type { JiraClientForSite } from '../authenticated-request' +import { attachmentContentUrl } from './content-url' +import { jiraRequestBinary } from '../authenticated-request' +import { JiraApiError } from '../jira-api-error' +import type { JiraAdfMediaAttrs, JiraAdfMediaResolver } from '../adf-markdown' +import { escapeMarkdownAlt, unresolvedMediaPlaceholder } from '../adf-markdown' +import { escapeMarkdownLinkDestination } from '../adf-media-destination' +import { loadAttachmentDataUrlWithCache } from './image-cache' import { MAX_IMAGE_BYTES, MAX_IMAGES, @@ -12,14 +13,14 @@ import { parseImageAttachmentMetas, isImageMimeType, type AttachmentMeta -} from './attachment-meta' -import { mapWithConcurrency } from '../../shared/map-with-concurrency' +} from './meta' +import { mapWithConcurrency } from '../../../shared/map-with-concurrency' import { JIRA_IMAGE_MAX_BYTES, JIRA_IMAGE_MAX_REFERENCES, type JiraMediaFormat -} from '../../shared/jira-attachment-image' -import { registerAttachmentImageGrant } from './attachment-image-grants' +} from '../../../shared/jira-attachment-image' +import { registerAttachmentImageGrant } from './image-grants' const DOWNLOAD_CONCURRENCY = 3 diff --git a/src/main/jira/attachment-legacy-cancellation.test.ts b/src/main/jira/attachments/legacy-cancellation.test.ts similarity index 87% rename from src/main/jira/attachment-legacy-cancellation.test.ts rename to src/main/jira/attachments/legacy-cancellation.test.ts index 6247e2c18fc..3c519652e4d 100644 --- a/src/main/jira/attachment-legacy-cancellation.test.ts +++ b/src/main/jira/attachments/legacy-cancellation.test.ts @@ -1,14 +1,20 @@ import { afterEach, beforeEach, expect, it, vi } from 'vitest' -import { readAttachmentImageBytes } from './attachment-image-bytes' -import { loadIssueImageAttachments, type JiraImageAttachment } from './attachment-images' -import * as cache from './attachment-image-cache' -import type { JiraClientForSite } from './authenticated-request' +import { readAttachmentImageBytes } from './image-bytes' +import { loadIssueImageAttachments, type JiraImageAttachment } from './images' +import * as cache from './image-cache' +import type { JiraClientForSite } from '../authenticated-request' const { binary } = vi.hoisted(() => ({ binary: vi.fn() })) -vi.mock('./authenticated-request', () => ({ +const { JiraApiError: JiraApiErrorMock } = vi.hoisted(() => ({ + JiraApiError: class extends Error {} +})) + +vi.mock('../jira-api-error', () => ({ JiraApiError: JiraApiErrorMock })) + +vi.mock('../authenticated-request', () => ({ apiBasePath: () => '/rest/api/3', jiraRequestBinary: binary, - JiraApiError: class extends Error {} + JiraApiError: JiraApiErrorMock })) const client: JiraClientForSite = { diff --git a/src/main/jira/attachment-meta.ts b/src/main/jira/attachments/meta.ts similarity index 100% rename from src/main/jira/attachment-meta.ts rename to src/main/jira/attachments/meta.ts diff --git a/src/main/jira/client-scoped-token.test.ts b/src/main/jira/client-scoped-token.test.ts index 1694a94769d..51b6b6fec15 100644 --- a/src/main/jira/client-scoped-token.test.ts +++ b/src/main/jira/client-scoped-token.test.ts @@ -91,7 +91,7 @@ describe('scoped Jira Cloud tokens over HTTP', () => { if (!client) { throw new Error('Expected connected Jira client') } - const cache = await import('./attachment-image-cache') + const cache = await import('./attachments/image-cache') cache.setCachedAttachmentDataUrl({ siteId: client.site.id, attachmentId: 'image', diff --git a/src/main/jira/client.ts b/src/main/jira/client.ts index 3f3ba25bde5..f829bb0ef9b 100644 --- a/src/main/jira/client.ts +++ b/src/main/jira/client.ts @@ -7,7 +7,7 @@ import type { JiraSiteSelection, JiraViewer } from '../../shared/jira-types' -import { clearAttachmentImagesForSite } from './attachment-image-cache' +import { clearAttachmentImagesForSite } from './attachments/image-cache' import { acquire, release } from './request-queue' import { getCredentialError, diff --git a/src/main/jira/issues.test.ts b/src/main/jira/issues.test.ts index a8c2f235715..0410aecbdef 100644 --- a/src/main/jira/issues.test.ts +++ b/src/main/jira/issues.test.ts @@ -28,11 +28,7 @@ vi.mock('./jira-agile-fields', () => ({ vi.mock('./request-queue', () => ({ acquire: acquireMock, release: releaseMock })) -vi.mock('./authenticated-request', () => ({ - apiBasePath: (site: { authType?: string }) => - site.authType === 'server' ? '/rest/api/2' : '/rest/api/3', - jiraRequest: (...args: unknown[]) => jiraRequestMock(...args), - jiraRequestBinary: (...args: unknown[]) => jiraRequestBinaryMock(...args), +const { JiraApiError: JiraApiErrorMock } = vi.hoisted(() => ({ JiraApiError: class JiraApiError extends Error { status: number | null constructor(message: string, status: number | null = null) { @@ -42,6 +38,16 @@ vi.mock('./authenticated-request', () => ({ } })) +vi.mock('./jira-api-error', () => ({ JiraApiError: JiraApiErrorMock })) + +vi.mock('./authenticated-request', () => ({ + apiBasePath: (site: { authType?: string }) => + site.authType === 'server' ? '/rest/api/2' : '/rest/api/3', + jiraRequest: (...args: unknown[]) => jiraRequestMock(...args), + jiraRequestBinary: (...args: unknown[]) => jiraRequestBinaryMock(...args), + JiraApiError: JiraApiErrorMock +})) + vi.mock('./client', () => ({ clearToken: (...args: unknown[]) => clearTokenMock(...args), getClients: (...args: unknown[]) => getClientsMock(...args), @@ -84,7 +90,7 @@ describe('Jira issue operations', () => { releaseMock.mockImplementation(() => {}) jiraRequestBinaryMock.mockReset() jiraRequestMock.mockReset() - const { _resetAttachmentImageCache } = await import('./attachment-image-cache') + const { _resetAttachmentImageCache } = await import('./attachments/image-cache') _resetAttachmentImageCache() }) diff --git a/src/main/jira/jira-issue-comments.ts b/src/main/jira/jira-issue-comments.ts index 88ffa0b9836..19d41761e35 100644 --- a/src/main/jira/jira-issue-comments.ts +++ b/src/main/jira/jira-issue-comments.ts @@ -17,7 +17,7 @@ import { import { extractAttachmentContentIdsFromHtml, selectPreferredAttachmentIds -} from './attachment-discovery' +} from './attachments/discovery' import { mapUser } from './jira-issue-mapping' import { flushMediaResolutionWarn, diff --git a/src/main/jira/jira-issue-media.ts b/src/main/jira/jira-issue-media.ts index 8ffd6ae4827..5f2beaacd8b 100644 --- a/src/main/jira/jira-issue-media.ts +++ b/src/main/jira/jira-issue-media.ts @@ -3,12 +3,12 @@ import { extractAttachmentContentIdsFromHtml, selectPreferredAttachmentIds, warnIfMediaResolutionIncomplete -} from './attachment-discovery' +} from './attachments/discovery' import { createMediaMarkdownResolver, loadIssueImageAttachments, type MediaResolutionStats -} from './attachment-images' +} from './attachments/images' import type { JiraClientForSite } from './authenticated-request' import { asRecord, asString, type JiraRecord } from './jira-record-pages' import { diff --git a/src/main/runtime/rpc/methods/jira-attachment-image.test.ts b/src/main/runtime/rpc/methods/jira-attachment-image.test.ts index ea9236de4ed..cfd2151360b 100644 --- a/src/main/runtime/rpc/methods/jira-attachment-image.test.ts +++ b/src/main/runtime/rpc/methods/jira-attachment-image.test.ts @@ -6,15 +6,15 @@ import { JIRA_ATTACHMENT_IMAGE_METHODS } from './jira-attachment-image' import { registerAttachmentImageGrant, resetAttachmentImageGrantsForTest -} from '../../../jira/attachment-image-grants' +} from '../../../jira/attachments/image-grants' import { clearAttachmentImagesForSite, _resetAttachmentImageCache -} from '../../../jira/attachment-image-cache' +} from '../../../jira/attachments/image-cache' import { decodeJiraImageFrame } from '../../../../shared/jira-image-stream-frame' const { read } = vi.hoisted(() => ({ read: vi.fn() })) -vi.mock('../../../jira/attachment-image-read', () => ({ readGrantedAttachmentImage: read })) +vi.mock('../../../jira/attachments/image-read', () => ({ readGrantedAttachmentImage: read })) const transferId = '12345678-1234-4234-8234-123456789012' const runtime = { getRuntimeId: () => 'runtime' } const dispatcher = new RpcDispatcher({ diff --git a/src/main/runtime/rpc/methods/jira-attachment-image.ts b/src/main/runtime/rpc/methods/jira-attachment-image.ts index f8e23c22287..e9c20c19a40 100644 --- a/src/main/runtime/rpc/methods/jira-attachment-image.ts +++ b/src/main/runtime/rpc/methods/jira-attachment-image.ts @@ -5,8 +5,8 @@ import { throwIfSignalAborted } from '../../../../shared/abort-signal-reason' import { attachmentImageGrant, isAttachmentImageGrantCurrent -} from '../../../jira/attachment-image-grants' -import { readGrantedAttachmentImage } from '../../../jira/attachment-image-read' +} from '../../../jira/attachments/image-grants' +import { readGrantedAttachmentImage } from '../../../jira/attachments/image-read' import { AttachmentImage } from '../../../../shared/rpc-contract/jira-params' import { defineStreamingMethod } from '../core' diff --git a/src/main/startup/main-process-preflight.ts b/src/main/startup/main-process-preflight.ts index bc3cd073eb9..4d98658718a 100644 --- a/src/main/startup/main-process-preflight.ts +++ b/src/main/startup/main-process-preflight.ts @@ -86,7 +86,7 @@ import { initOpenCodeUsagePath } from '../opencode-usage/store' import { initMuseUsagePath } from '../muse-usage/store' import { registerDocPreviewSchemePrivileges } from '../browser/doc-preview-protocol' import { MEDIA_PREVIEW_CUSTOM_SCHEME } from '../media/media-preview-protocol' -import { JIRA_IMAGE_CUSTOM_SCHEME } from '../jira/attachment-image-protocol' +import { JIRA_IMAGE_CUSTOM_SCHEME } from '../jira/attachments/image-protocol' import { startCrashpadCapture } from '../crash-reporting/crashpad-capture' import { CrashReportStore } from '../crash-reporting/crash-report-store' import { recordCrashBreadcrumb } from '../crash-reporting/crash-breadcrumb-store' diff --git a/src/main/startup/main-process-ready-foundation.ts b/src/main/startup/main-process-ready-foundation.ts index 38a32072b1e..807aff8be07 100644 --- a/src/main/startup/main-process-ready-foundation.ts +++ b/src/main/startup/main-process-ready-foundation.ts @@ -38,7 +38,7 @@ import { } from '../browser/browser-session-proxy' import { installDocPreviewProtocolHandler } from '../browser/doc-preview-protocol' import { installMediaPreviewProtocolHandler } from '../media/media-preview-protocol' -import { installJiraImageProtocolHandler } from '../jira/attachment-image-protocol' +import { installJiraImageProtocolHandler } from '../jira/attachments/image-protocol' import { registerDocPreviewGrantHandlers } from '../ipc/doc-preview-grant-ipc' import { initializeBrowserSessionsForApp } from '../browser/browser-session-startup' import { browserSessionRegistry } from '../browser/browser-session-registry'