kimi-code / apps /vis /server /test /lib /cron-store.test.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
7c5ed63 verified
Raw History Blame Contribute Delete
6.54 kB
import { mkdir, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { describe, it, expect, afterEach } from 'vitest';
import { buildSessionFixture } from '../fixtures/build';
import { isSafeCronId, listCronTasks } from '../../src/lib/cron-store';
async function writeCron(sessionDir: string, fileName: string, body: unknown): Promise<void> {
const dir = join(sessionDir, 'cron');
await mkdir(dir, { recursive: true });
await writeFile(join(dir, fileName), JSON.stringify(body));
}
describe('cron-store', () => {
let cleanup: (() => Promise<void>) | null = null;
afterEach(async () => { if (cleanup) await cleanup(); cleanup = null; });
it('lists valid cron tasks sorted by creation time', async () => {
const { sessionDir, cleanup: c } = await buildSessionFixture('sample-main');
cleanup = c;
// Written in the real on-disk shape — this doubles as a drift guard for
// the local CronTask mirror in agent-record-types.ts.
await writeCron(sessionDir, 'a1b2c3d4.json', {
id: 'a1b2c3d4', cron: '0 9 * * *', prompt: 'daily standup',
createdAt: 2000, recurring: true, lastFiredAt: 5000,
});
await writeCron(sessionDir, 'beefbeef.json', {
id: 'beefbeef', cron: '*/5 * * * *', prompt: 'poll ci',
createdAt: 1000, recurring: false,
});
const cron = await listCronTasks(sessionDir);
expect(cron.map((t) => t.id)).toEqual(['beefbeef', 'a1b2c3d4']); // createdAt asc
expect(cron[1]).toMatchObject({
cron: '0 9 * * *', prompt: 'daily standup', recurring: true, lastFiredAt: 5000,
});
});
it('skips bad ids, corrupt json, and records missing required fields', async () => {
const { sessionDir, cleanup: c } = await buildSessionFixture('sample-main');
cleanup = c;
await writeCron(sessionDir, 'NOTHEX12.json', { id: 'NOTHEX12', cron: 'x', prompt: 'p', createdAt: 1 });
await mkdir(join(sessionDir, 'cron'), { recursive: true });
await writeFile(join(sessionDir, 'cron', 'deadbeef.json'), '{ broken');
await writeCron(sessionDir, 'cafecafe.json', { id: 'cafecafe', cron: '* * * * *' }); // no prompt/createdAt
expect(await listCronTasks(sessionDir)).toEqual([]);
});
it('returns [] when there is no cron directory', async () => {
const { sessionDir, cleanup: c } = await buildSessionFixture('sample-main');
cleanup = c;
expect(await listCronTasks(sessionDir)).toEqual([]);
});
it('isSafeCronId accepts 8-hex and ULID ids (mirrors the engine regex)', () => {
expect(isSafeCronId('a1b2c3d4')).toBe(true);
expect(isSafeCronId('deadbeef')).toBe(true);
expect(isSafeCronId('DEADBEEF')).toBe(true);
expect(isSafeCronId('01ARZ3NDEKTSV4RRFFQ69G5FAV')).toBe(true);
expect(isSafeCronId('abc')).toBe(false);
expect(isSafeCronId('01ARZ3NDEKTSV4RRFFQ69G5FAO')).toBe(false); // 'O' is outside the ULID alphabet
expect(isSafeCronId('../escape')).toBe(false);
});
it('folds cron records from wire.jsonl (v2 layout)', async () => {
const { sessionDir, cleanup: c } = await buildSessionFixture('sample-main');
cleanup = c;
const taskA = {
id: '01ARZ3NDEKTSV4RRFFQ69G5FAV',
cron: '*/10 * * * *',
prompt: 'wire task a',
createdAt: 3000,
recurring: true,
};
const taskB = {
id: '01ARZ3NDEKTSV4RRFFQ69G5FB0',
cron: '0 8 * * *',
prompt: 'wire task b',
createdAt: 4000,
};
const lines = [
JSON.stringify({ type: 'metadata', protocol_version: '1.5', created_at: 1 }),
JSON.stringify({ type: 'cron.add', agentId: 'main', task: taskA, time: 10 }),
JSON.stringify({ type: 'cron.add', agentId: 'main', task: taskB, time: 11 }),
JSON.stringify({ type: 'cron.cursor', agentId: 'main', id: taskA.id, lastFiredAt: 9000, time: 12 }),
JSON.stringify({ type: 'cron.delete', agentId: 'main', ids: [taskB.id], time: 13 }),
];
await writeFile(join(sessionDir, 'wire.jsonl'), lines.join('\n') + '\n');
const cron = await listCronTasks(sessionDir);
expect(cron).toHaveLength(1);
expect(cron[0]).toMatchObject({ ...taskA, lastFiredAt: 9000 });
});
it('lets wire records win over legacy cron files for the same id', async () => {
const { sessionDir, cleanup: c } = await buildSessionFixture('sample-main');
cleanup = c;
await writeCron(sessionDir, 'a1b2c3d4.json', {
id: 'a1b2c3d4', cron: '0 9 * * *', prompt: 'file version', createdAt: 1000,
});
const lines = [
JSON.stringify({ type: 'metadata', protocol_version: '1.5', created_at: 1 }),
JSON.stringify({
type: 'cron.add',
agentId: 'main',
task: { id: 'a1b2c3d4', cron: '0 10 * * *', prompt: 'wire version', createdAt: 1000 },
time: 10,
}),
];
await writeFile(join(sessionDir, 'wire.jsonl'), lines.join('\n') + '\n');
const cron = await listCronTasks(sessionDir);
expect(cron).toHaveLength(1);
expect(cron[0]).toMatchObject({ cron: '0 10 * * *', prompt: 'wire version' });
});
it('treats a missing or unreadable wire.jsonl as no records', async () => {
const { sessionDir, cleanup: c } = await buildSessionFixture('sample-main');
cleanup = c;
await writeCron(sessionDir, 'a1b2c3d4.json', {
id: 'a1b2c3d4', cron: '0 9 * * *', prompt: 'only files', createdAt: 1000,
});
// No wire.jsonl at all — legacy sessions still list their files.
expect((await listCronTasks(sessionDir)).map((t) => t.id)).toEqual(['a1b2c3d4']);
});
it('ignores malformed cron.delete / cron.cursor payloads instead of throwing', async () => {
const { sessionDir, cleanup: c } = await buildSessionFixture('sample-main');
cleanup = c;
const lines = [
JSON.stringify({ type: 'metadata', protocol_version: '1.5', created_at: 1 }),
JSON.stringify({
type: 'cron.add',
agentId: 'main',
task: { id: '01ARZ3NDEKTSV4RRFFQ69G5FAV', cron: '* * * * *', prompt: 'p', createdAt: 1 },
time: 2,
}),
// Hand-edited / corrupted payloads: ids is not an array; cursor fields
// have the wrong types. The fold must skip them, not reject the read.
'{"type":"cron.delete","agentId":"main","ids":"not-an-array","time":3}',
'{"type":"cron.cursor","agentId":"main","id":42,"lastFiredAt":"soon","time":4}',
];
await writeFile(join(sessionDir, 'wire.jsonl'), lines.join('\n') + '\n');
const cron = await listCronTasks(sessionDir);
expect(cron.map((t) => t.id)).toEqual(['01ARZ3NDEKTSV4RRFFQ69G5FAV']);
});
});