import { beforeEach, describe, expect, mock, test } from 'bun:test' import * as common from './common.ts' type Call = [string, { method?: string, body?: unknown }?] const responses = new Map() const apiRequest = mock(async (endpoint: string, _options?: unknown): Promise => { for (const [prefix, response] of responses) { if (endpoint.startsWith(prefix)) return response } return {} }) void mock.module('./common.ts', () => ({ ...common, apiRequest })) function calls(): Call[] { return apiRequest.mock.calls as unknown as Call[] } /** The JSON the API receives for a call, serialized the way `apiRequestRaw` does it. */ function sent(index: number): Record { const body = calls()[index]![1]!.body return JSON.parse(typeof body === 'string' ? body : JSON.stringify(common.parsePayload(body))) } function openProject(overrides: Record = {}) { return { variables: [], teamIds: [], requiresTeam: false, allowAllWorkspaceTeams: false, ...overrides } } describe('resolveScheduleTeam', () => { test('an open project schedules for the whole workspace', async () => { const { resolveScheduleTeam } = await import('./schedules.ts') expect(resolveScheduleTeam(openProject(), undefined)).toBeNull() }) test('a restricted project with one eligible team picks it', async () => { const { resolveScheduleTeam } = await import('./schedules.ts') expect(resolveScheduleTeam(openProject({ requiresTeam: true, teamIds: ['t1'] }), undefined)).toBe('t1') }) test('several eligible teams is the user\'s choice, not a guess', async () => { const { resolveScheduleTeam } = await import('./schedules.ts') expect(() => resolveScheduleTeam(openProject({ requiresTeam: true, teamIds: ['t1', 't2'] }), undefined)) .toThrow('t1, t2') }) test('a restricted project refuses a workspace-wide schedule', async () => { const { resolveScheduleTeam } = await import('./schedules.ts') expect(() => resolveScheduleTeam(openProject({ requiresTeam: true, teamIds: ['t1'] }), null)).toThrow('restricted') }) }) describe('createSchedule', () => { beforeEach(() => { apiRequest.mockClear() responses.clear() }) test('posts a recurring schedule with the team the app would choose', async () => { responses.set('/v1/cron-schedules/targets/', openProject({ requiresTeam: true, teamIds: ['t1'] })) const { createSchedule } = await import('./schedules.ts') await createSchedule({ targetType: 'workflow', targetId: 'w1', name: 'Relatório de vendas — segunda 9h', scheduleType: 'recurring', cronExpression: '0 9 * * 1', timezone: 'America/Sao_Paulo', inputs: { region: 'Sul' }, }) expect(calls()[0]![0]).toBe('/v1/cron-schedules/targets/workflow/w1') const [endpoint, options] = calls()[1]! expect(endpoint).toBe('/v1/cron-schedules') expect(options!.method).toBe('POST') expect(sent(1)).toEqual({ scheduleType: 'recurring', cronExpression: '0 9 * * 1', timezone: 'America/Sao_Paulo', name: 'Relatório de vendas — segunda 9h', targetType: 'workflow', targetId: 'w1', teamId: 't1', inputs: { region: 'Sul' }, }) }) test('an open project is workspace-wide and no inputs sends no inputs key', async () => { responses.set('/v1/cron-schedules/targets/', openProject()) const { createSchedule } = await import('./schedules.ts') await createSchedule({ targetType: 'agent', targetId: 'a1', name: 'Relatório de sexta', scheduleType: 'once', runAt: '2026-10-09T12:00:00.000Z', timezone: 'America/Sao_Paulo', }) expect(calls()).toHaveLength(2) const body = sent(1) expect(body.teamId).toBeNull() expect('inputs' in body).toBe(false) }) test('an agent file input keeps vaultRef as a string', async () => { responses.set('/v1/cron-schedules/targets/', openProject()) const { createSchedule } = await import('./schedules.ts') await createSchedule({ targetType: 'agent', targetId: 'a1', name: 'Contratos — todo dia 8h', scheduleType: 'recurring', cronExpression: '0 8 * * *', timezone: 'America/Sao_Paulo', inputs: { contract: { vaultRef: 'vault://contract', filename: 'contrato.pdf' } }, }) expect(sent(1).inputs).toEqual({ contract: { vaultRef: 'vault://contract', filename: 'contrato.pdf' } }) }) }) describe('updateSchedule', () => { beforeEach(() => { apiRequest.mockClear() responses.clear() }) test('an agent file input keeps vaultRef as a string', async () => { const { updateSchedule } = await import('./schedules.ts') await updateSchedule('s1', { targetType: 'agent', inputs: { contract: { vaultRef: 'vault://contract', filename: 'contrato.pdf' } }, }) expect(calls()[0]![0]).toBe('/v1/cron-schedules/s1') expect(sent(0)).toEqual({ targetType: 'agent', inputs: { contract: { vaultRef: 'vault://contract', filename: 'contrato.pdf' } }, }) }) }) describe('listSchedules', () => { beforeEach(() => apiRequest.mockClear()) test('sends only the filters that were set', async () => { const { listSchedules } = await import('./schedules.ts') await listSchedules({ status: 'paused', attention: true, search: undefined }) expect(calls()[0]![0]).toBe('/v1/cron-schedules?status=paused&attention=true') }) })