basant307/AI_Governance_Project
048
1import * as fs from 'node:fs/promises';2import * as os from 'node:os';3import * as path from 'node:path';4import { afterEach, beforeEach, describe, expect, it } from 'vitest';5import { CronCreateTool } from './cron-create.js';6import { CronScheduler } from '../services/cronScheduler.js';7import { readCronTasks } from '../services/cronTasksFile.js';8import { Storage } from '../config/storage.js';9 10let tmpDir: string;11 12function makeConfig(maxAgeDays = 7) {13 const scheduler = new CronScheduler(tmpDir, maxAgeDays * 24 * 60 * 60 * 1000);14 return {15 getCronScheduler: () => scheduler,16 getCronRecurringMaxAgeDays: () => maxAgeDays,17 getProjectRoot: () => tmpDir,18 _scheduler: scheduler,19 } as unknown as import('../config/config.js').Config & {20 _scheduler: CronScheduler;21 };22}23 24describe('CronCreateTool', () => {25 let config: ReturnType<typeof makeConfig>;26 let tool: CronCreateTool;27 28 beforeEach(async () => {29 tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'cron-create-test-'));30 // Durable tasks persist under the user runtime dir, not the tree.31 Storage.setRuntimeBaseDir(tmpDir);32 config = makeConfig();33 tool = new CronCreateTool(config);34 });35 36 afterEach(async () => {37 Storage.setRuntimeBaseDir(null);38 await fs.rm(tmpDir, { recursive: true, force: true });39 });40 41 it('has the correct name', () => {42 expect(tool.name).toBe('cron_create');43 });44 45 it('creates a recurring job by default', async () => {46 const invocation = tool.build({47 cron: '*/5 * * * *',48 prompt: 'check status',49 });50 const result = await invocation.execute(new AbortController().signal);51 expect(result.error).toBeUndefined();52 expect(result.llmContent).toContain('Scheduled recurring job');53 expect(result.llmContent).toContain('Auto-expires after 7 days');54 expect(result.llmContent).toContain('Session-only');55 expect(config._scheduler.list()).toHaveLength(1);56 });57 58 it('reports a configured recurring max age in days', async () => {59 config = makeConfig(30);60 tool = new CronCreateTool(config);61 const invocation = tool.build({62 cron: '*/5 * * * *',63 prompt: 'check status',64 });65 const result = await invocation.execute(new AbortController().signal);66 expect(result.error).toBeUndefined();67 expect(result.llmContent).toContain('Auto-expires after 30 days');68 expect(tool.description).toContain('auto-expire after 30 days');69 });70 71 it('reports no expiry when the recurring max age is disabled', async () => {72 config = makeConfig(Infinity);73 tool = new CronCreateTool(config);74 const invocation = tool.build({75 cron: '*/5 * * * *',76 prompt: 'check status',77 });78 const result = await invocation.execute(new AbortController().signal);79 expect(result.error).toBeUndefined();80 expect(result.llmContent).toContain('Never auto-expires');81 expect(tool.description).toContain('never auto-expire');82 const job = config._scheduler.list()[0]!;83 expect(job.expiresAt).toBe(Infinity);84 });85 86 it('creates a one-shot job when recurring=false', async () => {87 const invocation = tool.build({88 cron: '*/1 * * * *',89 prompt: 'once',90 recurring: false,91 });92 const result = await invocation.execute(new AbortController().signal);93 expect(result.error).toBeUndefined();94 expect(result.llmContent).toContain('Scheduled one-shot task');95 expect(result.llmContent).toContain('fire once then auto-delete');96 const jobs = config._scheduler.list();97 expect(jobs).toHaveLength(1);98 expect(jobs[0]!.recurring).toBe(false);99 });100 101 it('creates a durable job and writes to disk', async () => {102 const invocation = tool.build({103 cron: '*/5 * * * *',104 prompt: 'durable check',105 durable: true,106 });107 const result = await invocation.execute(new AbortController().signal);108 expect(result.error).toBeUndefined();109 expect(result.llmContent).toContain(110 'Persisted to ~/.qwen/tmp/<project-hash>/scheduled_tasks.json',111 );112 expect(result.returnDisplay).toContain('[durable]');113 114 // Verify file was written115 const tasks = await readCronTasks(tmpDir);116 expect(tasks).toHaveLength(1);117 expect(tasks[0]!.prompt).toBe('durable check');118 });119 120 it('does not write to disk when durable=false', async () => {121 const invocation = tool.build({122 cron: '*/5 * * * *',123 prompt: 'session only',124 });125 await invocation.execute(new AbortController().signal);126 127 const tasks = await readCronTasks(tmpDir);128 expect(tasks).toHaveLength(0);129 });130 131 it.each(['', ' '])('rejects blank prompt %j', (prompt) => {132 expect(() =>133 tool.build({134 cron: '*/5 * * * *',135 prompt,136 }),137 ).toThrow('Parameter "prompt" must be a non-empty string.');138 expect(config._scheduler.list()).toHaveLength(0);139 });140 141 it('trims the prompt before scheduling the job', async () => {142 const invocation = tool.build({143 cron: '*/5 * * * *',144 prompt: ' check status ',145 });146 147 const result = await invocation.execute(new AbortController().signal);148 149 expect(result.error).toBeUndefined();150 const jobs = config._scheduler.list();151 expect(jobs).toHaveLength(1);152 expect(jobs[0]!.prompt).toBe('check status');153 });154 155 it('returns error for invalid cron expression', async () => {156 const invocation = tool.build({157 cron: 'bad cron',158 prompt: 'fail',159 });160 const result = await invocation.execute(new AbortController().signal);161 expect(result.error).toBeDefined();162 });163 164 it('rejects a cron that never matches a real date', async () => {165 // Parses fine, but Feb 30 never exists — accepting it would166 // schedule a job that silently never fires.167 const invocation = tool.build({168 cron: '0 0 30 2 *',169 prompt: 'never fires',170 });171 const result = await invocation.execute(new AbortController().signal);172 expect(result.error).toBeDefined();173 expect(result.llmContent).toContain('No matching fire time');174 expect(config._scheduler.list()).toHaveLength(0);175 });176 177 it('validates required params', () => {178 expect(() => tool.build({ cron: '*/1 * * * *' } as never)).toThrow();179 expect(() => tool.build({ prompt: 'test' } as never)).toThrow();180 });181});182 