Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 103 additions & 2 deletions packages/outpost/queue/src/__tests__/scheduler.test.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -22,3 +39,87 @@ describe('DEFAULT_SCHEDULED_JOBS', () => {
expect(definition!.intervalMs).toBe(5 * 60 * 1000);
});
});

describe('resolveScheduledJobs', () => {
let logSpy: ReturnType<typeof vi.spyOn>;

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<typeof vi.spyOn>;

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<string[]> {
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);
});
});
22 changes: 21 additions & 1 deletion packages/outpost/queue/src/scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -70,7 +90,7 @@ export class Scheduler {
private running = false;

constructor(definitions?: ScheduledJobDefinition[]) {
this.definitions = definitions ?? DEFAULT_SCHEDULED_JOBS;
this.definitions = definitions ?? resolveScheduledJobs();
}

/**
Expand Down
Loading