2026-08-06 09:28:31 +03:00
|
|
|
import { describe, expect, it, vi } from 'vitest';
|
2026-08-06 09:49:31 +03:00
|
|
|
import os from 'os';
|
|
|
|
|
import path from 'path';
|
|
|
|
|
import { mkdtemp, rm, writeFile } from 'fs/promises';
|
2026-08-06 09:28:31 +03:00
|
|
|
import { createScheduledTaskService } from './service.js';
|
|
|
|
|
|
|
|
|
|
const createService = (overrides = {}) => {
|
|
|
|
|
const projectConfigRuntime = {
|
|
|
|
|
listScheduledTasks: vi.fn(async () => []),
|
|
|
|
|
deleteScheduledTask: vi.fn(async () => ({ deleted: true, tasks: [] })),
|
|
|
|
|
...(overrides.projectConfigRuntime || {}),
|
|
|
|
|
};
|
|
|
|
|
const scheduledTasksRuntime = {
|
|
|
|
|
syncProject: vi.fn(async () => []),
|
|
|
|
|
...(overrides.scheduledTasksRuntime || {}),
|
|
|
|
|
};
|
|
|
|
|
const service = createScheduledTaskService({
|
|
|
|
|
readSettingsFromDiskMigrated: async () => ({
|
|
|
|
|
projects: [{ id: 'project-test', path: '/repo' }],
|
|
|
|
|
}),
|
|
|
|
|
sanitizeProjects: (projects) => projects,
|
|
|
|
|
projectConfigRuntime,
|
|
|
|
|
scheduledTasksRuntime,
|
|
|
|
|
});
|
|
|
|
|
return { service, projectConfigRuntime, scheduledTasksRuntime };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const loopTask = {
|
|
|
|
|
id: 'loop:project:daily-digest',
|
|
|
|
|
name: 'daily-digest',
|
|
|
|
|
enabled: true,
|
|
|
|
|
loopFile: '/repo/.agents/loops/daily-digest.md',
|
|
|
|
|
schedule: { kind: 'cron', cron: '0 9 * * *', timezone: 'UTC' },
|
|
|
|
|
execution: { prompt: 'digest', providerID: 'openai', modelID: 'gpt-4.1' },
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
describe('scheduled-task service remove', () => {
|
2026-08-06 09:49:31 +03:00
|
|
|
it('rejects deleting a loop-sourced task while its loop file still exists', async () => {
|
|
|
|
|
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'oc-loop-delete-'));
|
|
|
|
|
try {
|
|
|
|
|
const loopFilePath = path.join(tempRoot, 'daily.md');
|
|
|
|
|
await writeFile(loopFilePath, '---\nname: daily-digest\n---\nRun.\n', 'utf8');
|
2026-08-06 09:28:31 +03:00
|
|
|
|
2026-08-06 09:49:31 +03:00
|
|
|
const { service, projectConfigRuntime, scheduledTasksRuntime } = createService({
|
|
|
|
|
projectConfigRuntime: {
|
|
|
|
|
listScheduledTasks: vi.fn(async () => [{ ...loopTask, loopFile: loopFilePath }]),
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await expect(service.remove('project-test', loopTask.id)).rejects.toMatchObject({
|
|
|
|
|
statusCode: 400,
|
|
|
|
|
message: expect.stringContaining('delete the file to remove the task'),
|
|
|
|
|
});
|
|
|
|
|
expect(projectConfigRuntime.deleteScheduledTask).not.toHaveBeenCalled();
|
|
|
|
|
expect(scheduledTasksRuntime.syncProject).not.toHaveBeenCalled();
|
|
|
|
|
} finally {
|
|
|
|
|
await rm(tempRoot, { recursive: true, force: true });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('allows deleting a loop-sourced task once its loop file is gone', async () => {
|
|
|
|
|
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'oc-loop-delete-'));
|
|
|
|
|
try {
|
|
|
|
|
// The loop file was removed from disk; the orphan task is allowed to be
|
|
|
|
|
// deleted directly instead of waiting for the next reconcile.
|
|
|
|
|
const loopFilePath = path.join(tempRoot, 'gone.md');
|
|
|
|
|
|
|
|
|
|
const { service, projectConfigRuntime, scheduledTasksRuntime } = createService({
|
|
|
|
|
projectConfigRuntime: {
|
|
|
|
|
listScheduledTasks: vi.fn(async () => [{ ...loopTask, loopFile: loopFilePath }]),
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const tasks = await service.remove('project-test', loopTask.id);
|
|
|
|
|
|
|
|
|
|
expect(projectConfigRuntime.deleteScheduledTask).toHaveBeenCalledWith('project-test', loopTask.id);
|
|
|
|
|
expect(scheduledTasksRuntime.syncProject).toHaveBeenCalled();
|
|
|
|
|
expect(Array.isArray(tasks)).toBe(true);
|
|
|
|
|
} finally {
|
|
|
|
|
await rm(tempRoot, { recursive: true, force: true });
|
|
|
|
|
}
|
2026-08-06 09:28:31 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('deletes JSON-configured tasks normally', async () => {
|
|
|
|
|
const jsonTask = { ...loopTask, id: 'json-task', loopFile: undefined };
|
|
|
|
|
const { service, projectConfigRuntime, scheduledTasksRuntime } = createService({
|
|
|
|
|
projectConfigRuntime: {
|
|
|
|
|
listScheduledTasks: vi.fn(async () => [jsonTask]),
|
|
|
|
|
deleteScheduledTask: vi.fn(async () => ({ deleted: true, tasks: [] })),
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const tasks = await service.remove('project-test', jsonTask.id);
|
|
|
|
|
|
|
|
|
|
expect(projectConfigRuntime.deleteScheduledTask).toHaveBeenCalledWith('project-test', jsonTask.id);
|
|
|
|
|
expect(scheduledTasksRuntime.syncProject).toHaveBeenCalled();
|
|
|
|
|
expect(Array.isArray(tasks)).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
});
|