From 68e668b82afe1fd51198c70460775dbc87b7b761 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 24 Jun 2026 13:27:52 +0000 Subject: [PATCH 1/3] fix(notification): skip count validation without app id Co-authored-by: Neil Raina --- .../validateCount.service.test.ts | 49 +++++++++++ .../validate-count/validateCount.service.ts | 6 ++ src/config/index.ts | 8 +- src/utils/CopilotAPI.notifications.test.ts | 86 +++++++++++++++++++ src/utils/CopilotAPI.ts | 7 +- 5 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 src/app/api/notification/validate-count/validateCount.service.test.ts create mode 100644 src/utils/CopilotAPI.notifications.test.ts diff --git a/src/app/api/notification/validate-count/validateCount.service.test.ts b/src/app/api/notification/validate-count/validateCount.service.test.ts new file mode 100644 index 000000000..dde8c1f96 --- /dev/null +++ b/src/app/api/notification/validate-count/validateCount.service.test.ts @@ -0,0 +1,49 @@ +const ORIGINAL_ENV = process.env + +afterEach(() => { + jest.resetModules() + jest.dontMock('@/lib/db') + jest.dontMock('@/utils/CopilotAPI') + process.env = ORIGINAL_ENV +}) + +describe('ValidateCountService', () => { + it('skips reconciliation when the Tasks app id is not configured', async () => { + const getClientNotifications = jest.fn() + + jest.resetModules() + process.env = { + ...ORIGINAL_ENV, + COPILOT_API_KEY: 'api-key', + COPILOT_APP_ID: undefined, + COPILOT_APP_API_KEY: undefined, + NEXT_PUBLIC_ASSEMBLY_API_DOMAIN: 'https://api.example.com', + } + + jest.doMock('@/lib/db', () => ({ + __esModule: true, + default: { + getInstance: jest.fn(() => ({})), + }, + })) + jest.doMock('@/utils/CopilotAPI', () => ({ + CopilotAPI: jest.fn(() => ({ getClientNotifications })), + })) + + const { ValidateCountService } = await import('./validateCount.service') + const service = new ValidateCountService({ + token: 'token', + clientId: '11111111-1111-4111-8111-111111111111', + companyId: '22222222-2222-4222-8222-222222222222', + workspaceId: 'workspace-id', + } as any) + + await service.fixClientNotificationCount( + '11111111-1111-4111-8111-111111111111', + '22222222-2222-4222-8222-222222222222', + 'workspace-id', + ) + + expect(getClientNotifications).not.toHaveBeenCalled() + }) +}) diff --git a/src/app/api/notification/validate-count/validateCount.service.ts b/src/app/api/notification/validate-count/validateCount.service.ts index 4729a6238..c76ea8469 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 { APP_ID } 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 (!APP_ID) { + console.info('ValidateCount :: Skipping notification 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..d2b3504ea 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -38,7 +38,13 @@ 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 + +const parseOptionalUuid = (value?: string) => { + const parsed = z.string().uuid().safeParse(value) + return parsed.success ? parsed.data : undefined +} + +export const APP_ID = parseOptionalUuid(process.env.COPILOT_APP_ID ?? process.env.COPILOT_APP_API_KEY) 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..a03c2b585 --- /dev/null +++ b/src/utils/CopilotAPI.notifications.test.ts @@ -0,0 +1,86 @@ +const ORIGINAL_ENV = process.env + +const APP_ID = '11111111-1111-4111-8111-111111111111' +const OTHER_APP_ID = '22222222-2222-4222-8222-222222222222' +const COMPANY_ID = '33333333-3333-4333-8333-333333333333' +const OTHER_COMPANY_ID = '44444444-4444-4444-8444-444444444444' + +const loadCopilotAPI = async (env: Record = {}) => { + jest.resetModules() + process.env = { + ...ORIGINAL_ENV, + COPILOT_API_KEY: 'api-key', + NEXT_PUBLIC_ASSEMBLY_API_DOMAIN: 'https://api.example.com', + ...env, + } + + jest.doMock('copilot-node-sdk', () => ({ + copilotApi: jest.fn(() => ({})), + })) + + return import('@/utils/CopilotAPI') +} + +afterEach(() => { + jest.resetModules() + jest.dontMock('copilot-node-sdk') + process.env = ORIGINAL_ENV +}) + +describe('CopilotAPI#getClientNotifications', () => { + it('does not call Copilot when the Tasks app id is not configured', async () => { + const { CopilotAPI } = await loadCopilotAPI({ + COPILOT_APP_ID: undefined, + COPILOT_APP_API_KEY: undefined, + }) + const copilot = new CopilotAPI('token') as any + copilot.manualFetch = jest.fn() + + await expect(copilot._getClientNotifications('client-id', COMPANY_ID, 'workspace-id', { limit: 100 })).resolves.toEqual( + [], + ) + expect(copilot.manualFetch).not.toHaveBeenCalled() + }) + + it('filters Copilot notifications to the configured Tasks app and recipient company', async () => { + const { CopilotAPI } = await loadCopilotAPI({ COPILOT_APP_ID: APP_ID }) + const copilot = new CopilotAPI('token') as any + copilot.manualFetch = jest.fn().mockResolvedValue({ + data: [ + { + id: 'matching-recipient-company', + appId: APP_ID, + createdAt: '2026-06-24T00:00:00.000Z', + recipientCompanyId: COMPANY_ID, + }, + { id: 'matching-company', appId: APP_ID, createdAt: '2026-06-24T00:00:00.000Z', companyId: COMPANY_ID }, + { + id: 'different-company', + appId: APP_ID, + createdAt: '2026-06-24T00:00:00.000Z', + recipientCompanyId: OTHER_COMPANY_ID, + }, + { id: 'different-app', appId: OTHER_APP_ID, createdAt: '2026-06-24T00:00:00.000Z', recipientCompanyId: COMPANY_ID }, + ], + }) + + await expect(copilot._getClientNotifications('client-id', COMPANY_ID, 'workspace-id', { limit: 100 })).resolves.toEqual([ + { + id: 'matching-recipient-company', + appId: APP_ID, + createdAt: '2026-06-24T00:00:00.000Z', + recipientCompanyId: COMPANY_ID, + }, + { id: 'matching-company', appId: APP_ID, createdAt: '2026-06-24T00:00:00.000Z', companyId: COMPANY_ID }, + ]) + expect(copilot.manualFetch).toHaveBeenCalledWith( + 'notifications', + { + recipientClientId: 'client-id', + recipientCompanyId: COMPANY_ID, + limit: '100', + }, + 'workspace-id', + ) + }) +}) diff --git a/src/utils/CopilotAPI.ts b/src/utils/CopilotAPI.ts index dc2bf48b1..061bde3eb 100644 --- a/src/utils/CopilotAPI.ts +++ b/src/utils/CopilotAPI.ts @@ -292,6 +292,11 @@ export class CopilotAPI { } = { limit: 100 }, ) { console.info('CopilotAPI#_getClientNotifications', this.token) + if (!APP_ID) { + console.info('CopilotAPI#_getClientNotifications | Skipping lookup because COPILOT_APP_ID is not configured') + return [] + } + const response = await this.manualFetch( 'notifications', { @@ -304,7 +309,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 === APP_ID) .filter((notification) => { const isSameRecipientCompanyId = notification.recipientCompanyId && notification.recipientCompanyId === recipientCompanyId From 711840c966be7fb5272dbb203f74d8b99272979a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 24 Jun 2026 13:28:27 +0000 Subject: [PATCH 2/3] test(notification): mock retry in Copilot API tests 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 a03c2b585..6ed93bd9e 100644 --- a/src/utils/CopilotAPI.notifications.test.ts +++ b/src/utils/CopilotAPI.notifications.test.ts @@ -17,6 +17,9 @@ const loadCopilotAPI = async (env: Record = {}) => { jest.doMock('copilot-node-sdk', () => ({ copilotApi: jest.fn(() => ({})), })) + jest.doMock('@/app/api/core/utils/withRetry', () => ({ + withRetry: jest.fn((fn, args) => fn(...args)), + })) return import('@/utils/CopilotAPI') } @@ -24,6 +27,7 @@ const loadCopilotAPI = async (env: Record = {}) => { afterEach(() => { jest.resetModules() jest.dontMock('copilot-node-sdk') + jest.dontMock('@/app/api/core/utils/withRetry') process.env = ORIGINAL_ENV }) From 6b97bf65e1ae47fabe028ead280a9828c95330f3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 24 Jun 2026 13:29:39 +0000 Subject: [PATCH 3/3] test(notification): isolate env fixtures Co-authored-by: Neil Raina --- .../notification/validate-count/validateCount.service.test.ts | 2 ++ src/utils/CopilotAPI.notifications.test.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/app/api/notification/validate-count/validateCount.service.test.ts b/src/app/api/notification/validate-count/validateCount.service.test.ts index dde8c1f96..aa1bcb68a 100644 --- a/src/app/api/notification/validate-count/validateCount.service.test.ts +++ b/src/app/api/notification/validate-count/validateCount.service.test.ts @@ -47,3 +47,5 @@ describe('ValidateCountService', () => { expect(getClientNotifications).not.toHaveBeenCalled() }) }) + +export {} diff --git a/src/utils/CopilotAPI.notifications.test.ts b/src/utils/CopilotAPI.notifications.test.ts index 6ed93bd9e..2bcb4eac5 100644 --- a/src/utils/CopilotAPI.notifications.test.ts +++ b/src/utils/CopilotAPI.notifications.test.ts @@ -88,3 +88,5 @@ describe('CopilotAPI#getClientNotifications', () => { ) }) }) + +export {}