diff --git a/packages/outpost/queue/src/__tests__/scheduler.test.ts b/packages/outpost/queue/src/__tests__/scheduler.test.ts index e6e8471..79574fc 100644 --- a/packages/outpost/queue/src/__tests__/scheduler.test.ts +++ b/packages/outpost/queue/src/__tests__/scheduler.test.ts @@ -1,7 +1,24 @@ -import { describe, it, expect } from 'vitest'; -import { DEFAULT_SCHEDULED_JOBS } from '../scheduler.js'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +const { mockPrisma, mockCreateJob } = vi.hoisted(() => ({ + mockPrisma: { job: { findFirst: vi.fn() } }, + mockCreateJob: vi.fn(), +})); + +vi.mock('@copilotkit/outpost/db', () => ({ + prisma: mockPrisma, +})); + +vi.mock('../create-job.js', () => ({ + createJob: mockCreateJob, +})); + +import { DEFAULT_SCHEDULED_JOBS, Scheduler, resolveScheduledJobs } from '../scheduler.js'; import { JobType } from '../types.js'; +const HUBSPOT_SKIP_LINE = + '[Scheduler] Skipping HUBSPOT_SYNC: HUBSPOT_API_KEY is not set (set it to enable the HubSpot sync)'; + describe('DEFAULT_SCHEDULED_JOBS', () => { it('includes GITHUB_REACTION_POLL in the default scheduled jobs at a 24 hour interval', () => { const definition = DEFAULT_SCHEDULED_JOBS.find( @@ -22,3 +39,87 @@ describe('DEFAULT_SCHEDULED_JOBS', () => { expect(definition!.intervalMs).toBe(5 * 60 * 1000); }); }); + +describe('resolveScheduledJobs', () => { + let logSpy: ReturnType; + + beforeEach(() => { + logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + }); + + afterEach(() => { + logSpy.mockRestore(); + }); + + it.each([ + ['unset', {}], + ['empty', { HUBSPOT_API_KEY: '' }], + ['whitespace-only', { HUBSPOT_API_KEY: ' ' }], + ])('drops HUBSPOT_SYNC and logs one skip line when HUBSPOT_API_KEY is %s', (_label, env) => { + // Without a key every HUBSPOT_SYNC run fails at client construction and + // dead-letters, so the job is not scheduled at all. + const definitions = resolveScheduledJobs(DEFAULT_SCHEDULED_JOBS, env); + + expect(definitions.map((d) => d.type)).not.toContain(JobType.HUBSPOT_SYNC); + expect(definitions).toHaveLength(DEFAULT_SCHEDULED_JOBS.length - 1); + expect(logSpy).toHaveBeenCalledTimes(1); + expect(logSpy).toHaveBeenCalledWith(HUBSPOT_SKIP_LINE); + }); + + it('keeps every default job, HUBSPOT_SYNC included, when HUBSPOT_API_KEY is set', () => { + const definitions = resolveScheduledJobs(DEFAULT_SCHEDULED_JOBS, { + HUBSPOT_API_KEY: 'dummy-key', + }); + + expect(definitions).toEqual(DEFAULT_SCHEDULED_JOBS); + expect(logSpy).not.toHaveBeenCalled(); + }); +}); + +describe('Scheduler default definitions', () => { + const savedKey = process.env.HUBSPOT_API_KEY; + let logSpy: ReturnType; + + beforeEach(() => { + logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + mockPrisma.job.findFirst.mockReset().mockResolvedValue(null); + mockCreateJob.mockReset().mockResolvedValue('job-id'); + }); + + afterEach(() => { + logSpy.mockRestore(); + if (savedKey === undefined) delete process.env.HUBSPOT_API_KEY; + else process.env.HUBSPOT_API_KEY = savedKey; + }); + + async function startAndCollectCreatedTypes(): Promise { + const scheduler = new Scheduler(); + scheduler.start(); + scheduler.stop(); + // start() fires each tick without awaiting; let them settle. + await vi.waitFor(() => + expect(mockCreateJob).toHaveBeenCalledTimes(mockPrisma.job.findFirst.mock.calls.length), + ); + return mockCreateJob.mock.calls.map((call) => call[0] as string); + } + + it('never creates a HUBSPOT_SYNC job when HUBSPOT_API_KEY is unset', async () => { + delete process.env.HUBSPOT_API_KEY; + + const created = await startAndCollectCreatedTypes(); + + expect(created).not.toContain(JobType.HUBSPOT_SYNC); + expect(created).toHaveLength(DEFAULT_SCHEDULED_JOBS.length - 1); + expect(logSpy).toHaveBeenCalledWith(HUBSPOT_SKIP_LINE); + }); + + it('creates a HUBSPOT_SYNC job as before when HUBSPOT_API_KEY is set', async () => { + process.env.HUBSPOT_API_KEY = 'dummy-key'; + + const created = await startAndCollectCreatedTypes(); + + expect(created).toContain(JobType.HUBSPOT_SYNC); + expect(created).toHaveLength(DEFAULT_SCHEDULED_JOBS.length); + expect(logSpy).not.toHaveBeenCalledWith(HUBSPOT_SKIP_LINE); + }); +}); diff --git a/packages/outpost/queue/src/scheduler.ts b/packages/outpost/queue/src/scheduler.ts index ca50116..06fee87 100644 --- a/packages/outpost/queue/src/scheduler.ts +++ b/packages/outpost/queue/src/scheduler.ts @@ -57,6 +57,26 @@ export const DEFAULT_SCHEDULED_JOBS: ScheduledJobDefinition[] = [ }, ]; +/** + * Drop scheduled jobs whose required configuration is missing. + * + * HUBSPOT_SYNC needs HUBSPOT_API_KEY: without it every run fails at client + * construction and dead-letters. It is left out of the schedule (so no job rows + * are created) and one startup line says so. Setting the key re-enables it on + * the next start. + */ +export function resolveScheduledJobs( + definitions: ScheduledJobDefinition[] = DEFAULT_SCHEDULED_JOBS, + env: NodeJS.ProcessEnv = process.env, +): ScheduledJobDefinition[] { + if (env.HUBSPOT_API_KEY?.trim()) return definitions; + + console.log( + '[Scheduler] Skipping HUBSPOT_SYNC: HUBSPOT_API_KEY is not set (set it to enable the HubSpot sync)', + ); + return definitions.filter((d) => d.type !== JobType.HUBSPOT_SYNC); +} + /** * A simple interval-based scheduler that creates recurring jobs. * @@ -70,7 +90,7 @@ export class Scheduler { private running = false; constructor(definitions?: ScheduledJobDefinition[]) { - this.definitions = definitions ?? DEFAULT_SCHEDULED_JOBS; + this.definitions = definitions ?? resolveScheduledJobs(); } /**