import assert from "node:assert/strict"; import test from "node:test"; import * as campaignScheduling from "../lib/campaign-scheduling"; type CampaignSchedulingModule = { calculateNextRunAt: (input: { recurrence: string; status: "active" | "paused"; lastRunAt?: number | null; now?: number; }) => number | null; getCampaignCurrentRunStatus: ( input: { campaignStatus: "active" | "paused"; agentRuns?: Array<{ status: string; updatedAt?: number; }>; }, ) => string; isAllowedCampaignRecurrence?: (value: string) => boolean; CAMPAIGN_RECURRENCES?: readonly string[]; }; type SchedulingRun = { status: string; updatedAt?: number; }; const schedulingModule = campaignScheduling as unknown as CampaignSchedulingModule; function addDaysUTC(timestamp: number, days: number) { const date = new Date(timestamp); return Date.UTC( date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate() + days, date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds(), date.getUTCMilliseconds(), ); } function addMonthsUTC(timestamp: number, months: number) { const date = new Date(timestamp); return Date.UTC( date.getUTCFullYear(), date.getUTCMonth() + months, date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds(), date.getUTCMilliseconds(), ); } function expectFunction(value: unknown, name: string): T { assert.equal( typeof value, "function", `Expected ${name} to be implemented and exported`, ); return value as T; } function runWithLatest(updatedRuns: SchedulingRun[]) { return updatedRuns.sort((a, b) => { const aTime = typeof a.updatedAt === "number" ? a.updatedAt : 0; const bTime = typeof b.updatedAt === "number" ? b.updatedAt : 0; return bTime - aTime; }); } test("recurrence validation only allows manual/daily/weekly/monthly", () => { const allowed = ["manual", "daily", "weekly", "monthly"]; const forbidden = ["hourly", "2xweekly", "biweekly", ""]; if (typeof schedulingModule.isAllowedCampaignRecurrence === "function") { for (const value of allowed) { assert.equal( schedulingModule.isAllowedCampaignRecurrence(value), true, `${value} should be accepted`, ); } for (const value of forbidden) { assert.equal( schedulingModule.isAllowedCampaignRecurrence(value), false, `${value} should be rejected`, ); } return; } assert.ok( Array.isArray(schedulingModule.CAMPAIGN_RECURRENCES), "Expected recurrence validation helper or CAMPAIGN_RECURRENCES list", ); assert.deepEqual( [...schedulingModule.CAMPAIGN_RECURRENCES!].sort(), [...allowed].sort(), ); }); test("next run is null for manual or paused campaigns and scheduled for active recurring campaigns", () => { const calculateNextRunAt = expectFunction<(input: { recurrence: string; status: "active" | "paused"; lastRunAt?: number | null; now?: number; }) => number | null>(schedulingModule.calculateNextRunAt, "calculateNextRunAt"); const lastRunAt = Date.UTC(2026, 5, 1, 8, 0, 0); assert.equal( calculateNextRunAt({ recurrence: "manual", status: "active", lastRunAt, }), null, ); assert.equal( calculateNextRunAt({ recurrence: "daily", status: "paused", lastRunAt, }), null, ); const dailyNext = calculateNextRunAt({ recurrence: "daily", status: "active", lastRunAt, }); const weeklyNext = calculateNextRunAt({ recurrence: "weekly", status: "active", lastRunAt, }); const monthlyNext = calculateNextRunAt({ recurrence: "monthly", status: "active", lastRunAt, }); assert.equal(dailyNext, addDaysUTC(lastRunAt, 1)); assert.equal(weeklyNext, addDaysUTC(lastRunAt, 7)); assert.equal(monthlyNext, addMonthsUTC(lastRunAt, 1)); }); test("run status favors running/pending over finished states", () => { const getCampaignCurrentRunStatus = expectFunction< (input: { campaignStatus: "active" | "paused"; agentRuns?: Array<{ status: string; updatedAt?: number; }>; }) => string >(schedulingModule.getCampaignCurrentRunStatus, "getCampaignCurrentRunStatus"); const activeWithRunningAndFinished = runWithLatest([ { status: "succeeded", updatedAt: Date.UTC(2026, 5, 2, 10, 0, 0) }, { status: "running", updatedAt: Date.UTC(2026, 5, 2, 10, 5, 0) }, { status: "failed", updatedAt: Date.UTC(2026, 5, 2, 9, 50, 0) }, ]); const runningStatus = getCampaignCurrentRunStatus({ campaignStatus: "active", agentRuns: activeWithRunningAndFinished, }); assert.equal( runningStatus, "running", "Active running campaign should surface running as current status", ); const pendingOverFinished = runWithLatest([ { status: "succeeded", updatedAt: Date.UTC(2026, 5, 2, 10, 0, 0) }, { status: "pending", updatedAt: Date.UTC(2026, 5, 2, 10, 5, 0) }, { status: "failed", updatedAt: Date.UTC(2026, 5, 2, 9, 50, 0) }, ]); const pendingStatus = getCampaignCurrentRunStatus({ campaignStatus: "active", agentRuns: pendingOverFinished, }); assert.equal( pendingStatus, "pending", "Pending should outrank finished statuses when no running is active", ); const pausedWithoutRuns = getCampaignCurrentRunStatus({ campaignStatus: "paused", agentRuns: [], }); assert.equal( pausedWithoutRuns, "paused", "Paused campaigns should not report campaign activity by default", ); }); test("run status uses latest run first (running, pending, otherwise terminal)", () => { const getCampaignCurrentRunStatus = expectFunction< (input: { campaignStatus: "active" | "paused"; agentRuns?: Array<{ status: string; updatedAt?: number; }>; }) => string >(schedulingModule.getCampaignCurrentRunStatus, "getCampaignCurrentRunStatus"); const activeWithoutRuns = getCampaignCurrentRunStatus({ campaignStatus: "active", agentRuns: [], }); assert.equal(activeWithoutRuns, "idle"); const unsortedRuns = [ { status: "failed", updatedAt: Date.UTC(2026, 5, 1, 9, 0, 0) }, { status: "running", updatedAt: Date.UTC(2026, 5, 1, 8, 0, 0) }, { status: "failed", updatedAt: Date.UTC(2026, 5, 1, 7, 0, 0) }, ]; const latestRunWins = getCampaignCurrentRunStatus({ campaignStatus: "active", agentRuns: unsortedRuns, }); assert.equal( latestRunWins, "failed", "Latest status should determine current status, not any older run", ); const unsortedPending = [ { status: "running", updatedAt: Date.UTC(2026, 5, 1, 9, 0, 0) }, { status: "pending", updatedAt: Date.UTC(2026, 5, 1, 9, 5, 0) }, { status: "succeeded", updatedAt: Date.UTC(2026, 5, 1, 7, 0, 0) }, ]; const latestPending = getCampaignCurrentRunStatus({ campaignStatus: "active", agentRuns: unsortedPending, }); assert.equal( latestPending, "pending", "Pending latest run should be surfaced when it is the most recent.", ); const unsortedRunning = [ { status: "succeeded", updatedAt: Date.UTC(2026, 5, 1, 9, 0, 0) }, { status: "running", updatedAt: Date.UTC(2026, 5, 1, 10, 5, 0) }, { status: "pending", updatedAt: Date.UTC(2026, 5, 1, 8, 0, 0) }, ]; const latestRunning = getCampaignCurrentRunStatus({ campaignStatus: "active", agentRuns: unsortedRunning, }); assert.equal( latestRunning, "running", "Running should be surfaced when it is the latest run.", ); });