This commit is contained in:
231
packages/server/tests/routes/cron-api.test.ts
Normal file
231
packages/server/tests/routes/cron-api.test.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
/**
|
||||
* Cron REST API Route Tests
|
||||
*
|
||||
* Validates CRUD operations and manual trigger for the Solo cron service.
|
||||
* Uses the same pattern as trust-wiring.test.ts — tmpDir + buildLocalServer + server.inject().
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
import { MindDB } from '@waggle/core';
|
||||
import { buildLocalServer } from '../../src/local/index.js';
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { injectWithAuth } from '../test-utils.js';
|
||||
|
||||
describe('Cron REST API Routes', () => {
|
||||
let server: FastifyInstance;
|
||||
let tmpDir: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-cron-api-'));
|
||||
|
||||
// Create personal.mind
|
||||
const personalPath = path.join(tmpDir, 'personal.mind');
|
||||
const mind = new MindDB(personalPath);
|
||||
mind.close();
|
||||
|
||||
server = await buildLocalServer({ dataDir: tmpDir });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await server.close();
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('POST /api/cron creates a schedule and returns camelCase response', async () => {
|
||||
const res = await injectWithAuth(server, {
|
||||
method: 'POST',
|
||||
url: '/api/cron',
|
||||
payload: {
|
||||
name: 'Test job',
|
||||
cronExpr: '*/5 * * * *',
|
||||
jobType: 'memory_consolidation',
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const body = JSON.parse(res.body);
|
||||
expect(body.id).toBeDefined();
|
||||
expect(body.name).toBe('Test job');
|
||||
expect(body.cronExpr).toBe('*/5 * * * *');
|
||||
expect(body.nextRunAt).toBeDefined();
|
||||
expect(body.enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('POST /api/cron with invalid cron expression returns 400', async () => {
|
||||
const res = await injectWithAuth(server, {
|
||||
method: 'POST',
|
||||
url: '/api/cron',
|
||||
payload: {
|
||||
name: 'Bad cron',
|
||||
cronExpr: 'not-a-valid-cron',
|
||||
jobType: 'memory_consolidation',
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(400);
|
||||
const body = JSON.parse(res.body);
|
||||
expect(body.error).toBeDefined();
|
||||
});
|
||||
|
||||
it('GET /api/cron lists schedules including seeded defaults', async () => {
|
||||
const res = await injectWithAuth(server, {
|
||||
method: 'GET',
|
||||
url: '/api/cron',
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const body = JSON.parse(res.body);
|
||||
expect(body.schedules).toBeDefined();
|
||||
expect(Array.isArray(body.schedules)).toBe(true);
|
||||
// Should have at least the 2 seeded defaults + 1 created in previous test
|
||||
expect(body.schedules.length).toBeGreaterThanOrEqual(3);
|
||||
// Check that seeded defaults are present
|
||||
const names = body.schedules.map((s: { name: string }) => s.name);
|
||||
expect(names).toContain('Memory consolidation');
|
||||
expect(names).toContain('Workspace health check');
|
||||
});
|
||||
|
||||
it('PATCH /api/cron/:id updates name and enabled flag', async () => {
|
||||
// First create a schedule to update
|
||||
const createRes = await injectWithAuth(server, {
|
||||
method: 'POST',
|
||||
url: '/api/cron',
|
||||
payload: {
|
||||
name: 'To be updated',
|
||||
cronExpr: '0 12 * * *',
|
||||
jobType: 'workspace_health',
|
||||
},
|
||||
});
|
||||
const created = JSON.parse(createRes.body);
|
||||
|
||||
// Update it
|
||||
const res = await injectWithAuth(server, {
|
||||
method: 'PATCH',
|
||||
url: `/api/cron/${created.id}`,
|
||||
payload: {
|
||||
name: 'Updated name',
|
||||
enabled: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const body = JSON.parse(res.body);
|
||||
expect(body.name).toBe('Updated name');
|
||||
expect(body.enabled).toBe(false);
|
||||
});
|
||||
|
||||
it('DELETE /api/cron/:id removes schedule; subsequent GET returns 404', async () => {
|
||||
// Create a schedule to delete
|
||||
const createRes = await injectWithAuth(server, {
|
||||
method: 'POST',
|
||||
url: '/api/cron',
|
||||
payload: {
|
||||
name: 'To be deleted',
|
||||
cronExpr: '0 0 * * *',
|
||||
jobType: 'memory_consolidation',
|
||||
},
|
||||
});
|
||||
const created = JSON.parse(createRes.body);
|
||||
|
||||
// Delete it
|
||||
const delRes = await injectWithAuth(server, {
|
||||
method: 'DELETE',
|
||||
url: `/api/cron/${created.id}`,
|
||||
});
|
||||
expect(delRes.statusCode).toBe(200);
|
||||
|
||||
// Verify it's gone
|
||||
const getRes = await injectWithAuth(server, {
|
||||
method: 'GET',
|
||||
url: `/api/cron/${created.id}`,
|
||||
});
|
||||
expect(getRes.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
it('POST /api/cron/:id/trigger manually triggers a schedule', async () => {
|
||||
// Create a schedule to trigger
|
||||
const createRes = await injectWithAuth(server, {
|
||||
method: 'POST',
|
||||
url: '/api/cron',
|
||||
payload: {
|
||||
name: 'Manual trigger test',
|
||||
cronExpr: '0 0 1 1 *', // yearly — not naturally due
|
||||
jobType: 'memory_consolidation',
|
||||
},
|
||||
});
|
||||
const created = JSON.parse(createRes.body);
|
||||
|
||||
const res = await injectWithAuth(server, {
|
||||
method: 'POST',
|
||||
url: `/api/cron/${created.id}/trigger`,
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const body = JSON.parse(res.body);
|
||||
expect(body.triggered).toBe(true);
|
||||
});
|
||||
|
||||
// M-43 / P25 regression: triggering a disabled job must auto-enable it
|
||||
// before executing and return the enabled state to the client so the UI
|
||||
// toggle stops visually snapping back to off.
|
||||
it('POST /api/cron/:id/trigger auto-enables a disabled schedule', async () => {
|
||||
const createRes = await injectWithAuth(server, {
|
||||
method: 'POST',
|
||||
url: '/api/cron',
|
||||
payload: {
|
||||
name: 'Auto-enable on trigger',
|
||||
cronExpr: '0 0 1 1 *',
|
||||
jobType: 'memory_consolidation',
|
||||
enabled: false,
|
||||
},
|
||||
});
|
||||
const created = JSON.parse(createRes.body);
|
||||
expect(created.enabled).toBe(false);
|
||||
|
||||
const triggerRes = await injectWithAuth(server, {
|
||||
method: 'POST',
|
||||
url: `/api/cron/${created.id}/trigger`,
|
||||
});
|
||||
expect(triggerRes.statusCode).toBe(200);
|
||||
const body = JSON.parse(triggerRes.body);
|
||||
expect(body.triggered).toBe(true);
|
||||
expect(body.autoEnabled).toBe(true);
|
||||
expect(body.schedule).toBeDefined();
|
||||
expect(body.schedule.enabled).toBe(true);
|
||||
|
||||
// Persisted: subsequent GET reflects enabled state.
|
||||
const getRes = await injectWithAuth(server, {
|
||||
method: 'GET',
|
||||
url: `/api/cron/${created.id}`,
|
||||
});
|
||||
expect(getRes.statusCode).toBe(200);
|
||||
const fresh = JSON.parse(getRes.body);
|
||||
expect(fresh.enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('POST /api/cron/:id/trigger does not set autoEnabled when job is already enabled', async () => {
|
||||
const createRes = await injectWithAuth(server, {
|
||||
method: 'POST',
|
||||
url: '/api/cron',
|
||||
payload: {
|
||||
name: 'Already enabled trigger',
|
||||
cronExpr: '0 0 1 1 *',
|
||||
jobType: 'memory_consolidation',
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
const created = JSON.parse(createRes.body);
|
||||
|
||||
const triggerRes = await injectWithAuth(server, {
|
||||
method: 'POST',
|
||||
url: `/api/cron/${created.id}/trigger`,
|
||||
});
|
||||
const body = JSON.parse(triggerRes.body);
|
||||
expect(body.triggered).toBe(true);
|
||||
expect(body.autoEnabled).toBe(false);
|
||||
expect(body.schedule?.enabled).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user