From b87c643327d4e75ddda9b4bcdd3b729d835fc0d5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 24 Jun 2026 13:03:34 +0000 Subject: [PATCH 1/3] fix: skip notification validation without app id Co-authored-by: Neil Raina --- .../ValidateNotificationCountFetcher.tsx | 7 +- .../validate-count/validateCount.service.ts | 6 ++ src/config/index.ts | 3 +- src/utils/CopilotAPI.notifications.test.ts | 80 +++++++++++++++++++ src/utils/CopilotAPI.ts | 10 ++- 5 files changed, 102 insertions(+), 4 deletions(-) create mode 100644 src/utils/CopilotAPI.notifications.test.ts diff --git a/src/app/_fetchers/ValidateNotificationCountFetcher.tsx b/src/app/_fetchers/ValidateNotificationCountFetcher.tsx index 56cf2d4a0..57ed87790 100644 --- a/src/app/_fetchers/ValidateNotificationCountFetcher.tsx +++ b/src/app/_fetchers/ValidateNotificationCountFetcher.tsx @@ -1,7 +1,12 @@ -import { apiUrl } from '@/config' +import { apiUrl, getCopilotAppId } from '@/config' import { PropsWithToken } from '@/types/interfaces' export const ValidateNotificationCountFetcher = async ({ token }: PropsWithToken) => { + if (!getCopilotAppId()) { + console.warn('Validate notifications skipped: Copilot app id is not configured') + return <> + } + try { await fetch(`${apiUrl}/api/notification/validate-count?token=${token}`) } catch (err) { diff --git a/src/app/api/notification/validate-count/validateCount.service.ts b/src/app/api/notification/validate-count/validateCount.service.ts index 4729a6238..7aa95ea5f 100644 --- a/src/app/api/notification/validate-count/validateCount.service.ts +++ b/src/app/api/notification/validate-count/validateCount.service.ts @@ -1,4 +1,5 @@ import { MAX_NOTIFICATIONS_COUNT } from '@/constants/notifications' +import { getCopilotAppId } from '@/config' import { DuplicateNotificationsQuerySchema } from '@/types/client-notifications' import { getArrayDifference } from '@/utils/array' import { copilotBottleneck } from '@/utils/bottleneck' @@ -13,6 +14,11 @@ export class ValidateCountService extends NotificationService { * @param {string} clientId - Copilot client id for which notification fix has to be done */ async fixClientNotificationCount(clientId: string, companyId: string, workspaceId: string): Promise { + if (!getCopilotAppId()) { + console.warn('ValidateCount :: Skipping notification count validation because Copilot app id is not configured') + return + } + const notifications = await this.copilot.getClientNotifications(clientId, companyId, workspaceId, { limit: MAX_NOTIFICATIONS_COUNT, }) diff --git a/src/config/index.ts b/src/config/index.ts index 26abc1871..8ca74134a 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -38,7 +38,8 @@ export const supabaseBucket = process.env.NEXT_PUBLIC_SUPABASE_BUCKET || '' // (OUT-3864). Empty falls back to the project URL, so behaviour is unchanged until it's configured. export const supabaseStorageDomain = process.env.NEXT_PUBLIC_SUPABASE_STORAGE_DOMAIN || '' export const cronSecret = process.env.CRON_SECRET || '' -export const APP_ID = process.env.COPILOT_APP_API_KEY +export const getCopilotAppId = () => process.env.COPILOT_APP_ID || process.env.COPILOT_APP_API_KEY || '' +export const APP_ID = getCopilotAppId() export const ScrapImageExpiryPeriod = +(process.env.SCRAP_IMAGE_EXPIRY_PERIOD || '604800000') diff --git a/src/utils/CopilotAPI.notifications.test.ts b/src/utils/CopilotAPI.notifications.test.ts new file mode 100644 index 000000000..eff157303 --- /dev/null +++ b/src/utils/CopilotAPI.notifications.test.ts @@ -0,0 +1,80 @@ +const ORIGINAL_ENV = process.env + +const APP_ID = '00000000-0000-4000-8000-000000000001' +const OTHER_APP_ID = '00000000-0000-4000-8000-000000000002' + +const loadCopilotAPI = async () => { + jest.resetModules() + jest.doMock('copilot-node-sdk', () => ({ + copilotApi: jest.fn(() => ({})), + })) + + return await import('@/utils/CopilotAPI') +} + +const notification = (overrides: Record) => ({ + id: 'notification-id', + appId: APP_ID, + createdAt: '2026-06-24T00:00:00.000Z', + ...overrides, +}) + +describe('CopilotAPI client notification filtering', () => { + beforeEach(() => { + process.env = { + ...ORIGINAL_ENV, + COPILOT_API_KEY: 'api-key', + NEXT_PUBLIC_ASSEMBLY_API_DOMAIN: 'https://api.example.com', + } + delete process.env.COPILOT_APP_ID + delete process.env.COPILOT_APP_API_KEY + }) + + afterEach(() => { + jest.dontMock('copilot-node-sdk') + }) + + afterAll(() => { + process.env = ORIGINAL_ENV + }) + + it('skips notification lookup when no Copilot app id is configured', async () => { + const { CopilotAPI } = await loadCopilotAPI() + const copilot = new CopilotAPI('token') + const manualFetch = jest.fn() + copilot.manualFetch = manualFetch + + const result = await copilot.getClientNotifications('client-1', 'company-1', 'workspace-1') + + expect(result).toEqual([]) + expect(manualFetch).not.toHaveBeenCalled() + }) + + it('filters Copilot notifications to the configured app and company', async () => { + process.env.COPILOT_APP_ID = APP_ID + const { CopilotAPI } = await loadCopilotAPI() + const copilot = new CopilotAPI('token') + const manualFetch = jest.fn().mockResolvedValue({ + data: [ + notification({ id: 'matching-recipient-company', recipientCompanyId: 'company-1' }), + notification({ id: 'matching-task-company', companyId: 'company-1' }), + notification({ id: 'other-app', appId: OTHER_APP_ID, recipientCompanyId: 'company-1' }), + notification({ id: 'other-company', recipientCompanyId: 'company-2' }), + ], + }) + copilot.manualFetch = manualFetch + + const result = await copilot.getClientNotifications('client-1', 'company-1', 'workspace-1', { limit: 25 }) + + expect(manualFetch).toHaveBeenCalledWith( + 'notifications', + { + recipientClientId: 'client-1', + recipientCompanyId: 'company-1', + limit: '25', + }, + 'workspace-1', + ) + expect(result.map(({ id }) => id)).toEqual(['matching-recipient-company', 'matching-task-company']) + }) +}) diff --git a/src/utils/CopilotAPI.ts b/src/utils/CopilotAPI.ts index dc2bf48b1..64ec5a98a 100644 --- a/src/utils/CopilotAPI.ts +++ b/src/utils/CopilotAPI.ts @@ -1,6 +1,6 @@ import APIError from '@/app/api/core/exceptions/api' import { withRetry } from '@/app/api/core/utils/withRetry' -import { copilotAPIKey as apiKey, APP_ID, assemblyApiDomain } from '@/config' +import { copilotAPIKey as apiKey, assemblyApiDomain, getCopilotAppId } from '@/config' import { MAX_LIMIT_CLIENT_COUNT } from '@/constants/users' import { AssemblyMetadata, @@ -292,6 +292,12 @@ export class CopilotAPI { } = { limit: 100 }, ) { console.info('CopilotAPI#_getClientNotifications', this.token) + const appId = getCopilotAppId() + if (!appId) { + console.warn('CopilotAPI#_getClientNotifications | Skipping because Copilot app id is not configured') + return [] + } + const response = await this.manualFetch( 'notifications', { @@ -304,7 +310,7 @@ export class CopilotAPI { const notifications = z.array(NotificationCreatedResponseSchema).parse(response.data) // Return only all notifications triggered by tasks-app return notifications - .filter((notification) => notification.appId === z.string({ message: 'Missing AppID in environment' }).parse(APP_ID)) + .filter((notification) => notification.appId === appId) .filter((notification) => { const isSameRecipientCompanyId = notification.recipientCompanyId && notification.recipientCompanyId === recipientCompanyId From 61fadb48fd25ec87434f6e0757f77f25255e817e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 24 Jun 2026 13:04:02 +0000 Subject: [PATCH 2/3] test: mock retry wrapper for notification filtering Co-authored-by: Neil Raina --- src/utils/CopilotAPI.notifications.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/utils/CopilotAPI.notifications.test.ts b/src/utils/CopilotAPI.notifications.test.ts index eff157303..150541340 100644 --- a/src/utils/CopilotAPI.notifications.test.ts +++ b/src/utils/CopilotAPI.notifications.test.ts @@ -5,6 +5,9 @@ const OTHER_APP_ID = '00000000-0000-4000-8000-000000000002' const loadCopilotAPI = async () => { jest.resetModules() + jest.doMock('@/app/api/core/utils/withRetry', () => ({ + withRetry: (fn: (...args: unknown[]) => unknown, args: unknown[]) => fn(...args), + })) jest.doMock('copilot-node-sdk', () => ({ copilotApi: jest.fn(() => ({})), })) @@ -31,6 +34,7 @@ describe('CopilotAPI client notification filtering', () => { }) afterEach(() => { + jest.dontMock('@/app/api/core/utils/withRetry') jest.dontMock('copilot-node-sdk') }) From 1ba5ce09d7023de283a9084608cd04e137dcc582 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 24 Jun 2026 13:05:21 +0000 Subject: [PATCH 3/3] test: cover notification count fetcher app id guard Co-authored-by: Neil Raina --- .../ValidateNotificationCountFetcher.test.tsx | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/app/_fetchers/ValidateNotificationCountFetcher.test.tsx diff --git a/src/app/_fetchers/ValidateNotificationCountFetcher.test.tsx b/src/app/_fetchers/ValidateNotificationCountFetcher.test.tsx new file mode 100644 index 000000000..73724eadd --- /dev/null +++ b/src/app/_fetchers/ValidateNotificationCountFetcher.test.tsx @@ -0,0 +1,38 @@ +import { getCopilotAppId } from '@/config' +import { ValidateNotificationCountFetcher } from './ValidateNotificationCountFetcher' + +jest.mock('@/config', () => ({ + apiUrl: 'https://tasks.example.com', + getCopilotAppId: jest.fn(), +})) + +const mockGetCopilotAppId = getCopilotAppId as jest.Mock + +describe('ValidateNotificationCountFetcher', () => { + const originalFetch = global.fetch + + beforeEach(() => { + jest.clearAllMocks() + global.fetch = jest.fn().mockResolvedValue({ ok: true } as Response) + }) + + afterAll(() => { + global.fetch = originalFetch + }) + + it('does not call validate-count when Copilot app id is missing', async () => { + mockGetCopilotAppId.mockReturnValue('') + + await ValidateNotificationCountFetcher({ token: 'client-token' }) + + expect(global.fetch).not.toHaveBeenCalled() + }) + + it('calls validate-count when Copilot app id is configured', async () => { + mockGetCopilotAppId.mockReturnValue('app-id') + + await ValidateNotificationCountFetcher({ token: 'client-token' }) + + expect(global.fetch).toHaveBeenCalledWith('https://tasks.example.com/api/notification/validate-count?token=client-token') + }) +})