Files
open-design/apps/daemon/tests/routine-routes.test.ts
shangxinyu1andGitHub 0220124e0f test: add Memory and Routines coverage (#1400)
* test: align extended Playwright coverage with current UI behavior

* test: address extended suite review feedback

* test: fix Codex fallback config hydration in e2e

* test: add Memory and Routines coverage

* test: fix Memory and Routines component test typing

* test: include Memory and Routines e2e in extended suite
2026-05-12 17:48:56 +08:00

321 lines
11 KiB
TypeScript

import express from 'express';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { mkdtempSync, rmSync } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import {
closeDatabase,
getRoutine,
insertProject,
insertRoutineRun,
openDatabase,
} from '../src/db.js';
import { registerRoutineRoutes } from '../src/routine-routes.js';
describe('routine routes', () => {
let tempDir: string;
async function listen(app: express.Express) {
const server = app.listen(0, '127.0.0.1');
await new Promise<void>((resolve, reject) => {
server.once('listening', () => resolve());
server.once('error', reject);
});
const address = server.address();
if (!address || typeof address === 'string') {
throw new Error('failed to resolve test server port');
}
return {
server,
port: address.port,
};
}
beforeEach(() => {
tempDir = mkdtempSync(path.join(os.tmpdir(), 'od-routine-routes-'));
});
afterEach(() => {
closeDatabase();
rmSync(tempDir, { recursive: true, force: true });
vi.restoreAllMocks();
});
function buildApp() {
const db = openDatabase(tempDir, { dataDir: tempDir });
const nextRunAt = vi.fn(() => new Date('2026-05-13T01:00:00.000Z'));
const rescheduleOne = vi.fn();
const unschedule = vi.fn();
const runNow = vi.fn(async (routineId: string) => {
insertRoutineRun(db, {
id: 'run-1',
routineId,
trigger: 'manual',
status: 'queued',
projectId: 'proj-run',
conversationId: 'conv-run',
agentRunId: 'agent-run-1',
startedAt: Date.now(),
});
return {
projectId: 'proj-run',
conversationId: 'conv-run',
agentRunId: 'agent-run-1',
completion: Promise.resolve({ status: 'queued' }),
};
});
const app = express();
app.use(express.json());
registerRoutineRoutes(app, {
db,
routines: {
routineService: {
nextRunAt,
rescheduleOne,
runNow,
unschedule,
},
},
} as any);
return { app, db, nextRunAt, rescheduleOne, runNow, unschedule };
}
it('creates a reuse-mode routine and includes the computed next run', async () => {
const { app, db, rescheduleOne } = buildApp();
const now = Date.now();
insertProject(db, {
id: 'proj-1',
name: 'Routine target',
createdAt: now,
updatedAt: now,
});
const { server, port } = await listen(app);
try {
const res = await fetch(`http://127.0.0.1:${port}/api/routines`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
name: 'Weekly digest',
prompt: 'Summarize GitHub and design activity.',
schedule: {
kind: 'weekly',
weekday: 3,
time: '09:00',
timezone: 'UTC',
},
target: { mode: 'reuse', projectId: 'proj-1' },
enabled: true,
}),
});
expect(res.status).toBe(201);
const json = await res.json() as {
routine: {
id: string;
name: string;
target: { mode: string; projectId: string };
nextRunAt: number;
};
};
expect(json.routine.name).toBe('Weekly digest');
expect(json.routine.target).toEqual({ mode: 'reuse', projectId: 'proj-1' });
expect(json.routine.nextRunAt).toBe(new Date('2026-05-13T01:00:00.000Z').getTime());
const stored = getRoutine(db, json.routine.id);
expect(stored?.projectId).toBe('proj-1');
expect(rescheduleOne).toHaveBeenCalledWith(json.routine.id);
} finally {
await new Promise<void>((resolve) => server.close(() => resolve()));
}
});
it('patches enabled state and target mode, then reschedules the routine', async () => {
const { app, db, rescheduleOne } = buildApp();
const now = Date.now();
insertProject(db, {
id: 'proj-1',
name: 'Routine target',
createdAt: now,
updatedAt: now,
});
const { server: createServer, port } = await listen(app);
try {
const createRes = await fetch(`http://127.0.0.1:${port}/api/routines`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
name: 'Daily digest',
prompt: 'Summarize activity.',
schedule: { kind: 'daily', time: '09:00', timezone: 'UTC' },
target: { mode: 'create_each_run' },
enabled: true,
}),
});
const created = await createRes.json() as { routine: { id: string } };
const patchRes = await fetch(`http://127.0.0.1:${port}/api/routines/${created.routine.id}`, {
method: 'PATCH',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
enabled: false,
target: { mode: 'reuse', projectId: 'proj-1' },
}),
});
expect(patchRes.status).toBe(200);
const patched = await patchRes.json() as {
routine: { enabled: boolean; target: { mode: string; projectId: string } };
};
expect(patched.routine.enabled).toBe(false);
expect(patched.routine.target).toEqual({ mode: 'reuse', projectId: 'proj-1' });
expect(rescheduleOne).toHaveBeenLastCalledWith(created.routine.id);
} finally {
await new Promise<void>((resolve) => createServer.close(() => resolve()));
}
});
it('runs a routine now and exposes its run history', async () => {
const { app, runNow } = buildApp();
const { server, port } = await listen(app);
try {
const createRes = await fetch(`http://127.0.0.1:${port}/api/routines`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
name: 'Daily digest',
prompt: 'Summarize activity.',
schedule: { kind: 'daily', time: '09:00', timezone: 'UTC' },
target: { mode: 'create_each_run' },
enabled: true,
}),
});
const created = await createRes.json() as { routine: { id: string } };
const runRes = await fetch(`http://127.0.0.1:${port}/api/routines/${created.routine.id}/run`, {
method: 'POST',
});
expect(runRes.status).toBe(202);
const runJson = await runRes.json() as {
projectId: string;
conversationId: string;
agentRunId: string;
run: { status: string; trigger: string };
};
expect(runJson.projectId).toBe('proj-run');
expect(runJson.conversationId).toBe('conv-run');
expect(runJson.agentRunId).toBe('agent-run-1');
expect(runJson.run.status).toBe('queued');
expect(runNow).toHaveBeenCalledWith(created.routine.id);
const runsRes = await fetch(`http://127.0.0.1:${port}/api/routines/${created.routine.id}/runs?limit=10`);
expect(runsRes.status).toBe(200);
const runsJson = await runsRes.json() as { runs: Array<{ id: string; status: string }> };
expect(runsJson.runs).toHaveLength(1);
expect(runsJson.runs[0]).toMatchObject({ id: 'run-1', status: 'queued' });
} finally {
await new Promise<void>((resolve) => server.close(() => resolve()));
}
});
it('rejects reuse-mode creation when the target project does not exist', async () => {
const { app } = buildApp();
const { server, port } = await listen(app);
try {
const res = await fetch(`http://127.0.0.1:${port}/api/routines`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
name: 'Weekly digest',
prompt: 'Summarize GitHub and design activity.',
schedule: {
kind: 'weekly',
weekday: 3,
time: '09:00',
timezone: 'UTC',
},
target: { mode: 'reuse', projectId: 'missing-project' },
enabled: true,
}),
});
expect(res.status).toBe(400);
const json = await res.json() as { error: string };
expect(json.error).toContain('target project missing-project not found');
} finally {
await new Promise<void>((resolve) => server.close(() => resolve()));
}
});
it('deletes a routine and unschedules it', async () => {
const { app, unschedule } = buildApp();
const { server, port } = await listen(app);
try {
const createRes = await fetch(`http://127.0.0.1:${port}/api/routines`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
name: 'Daily digest',
prompt: 'Summarize activity.',
schedule: { kind: 'daily', time: '09:00', timezone: 'UTC' },
target: { mode: 'create_each_run' },
enabled: true,
}),
});
const created = await createRes.json() as { routine: { id: string } };
const deleteRes = await fetch(`http://127.0.0.1:${port}/api/routines/${created.routine.id}`, {
method: 'DELETE',
});
expect(deleteRes.status).toBe(204);
expect(unschedule).toHaveBeenCalledWith(created.routine.id);
const getRes = await fetch(`http://127.0.0.1:${port}/api/routines/${created.routine.id}`);
expect(getRes.status).toBe(404);
} finally {
await new Promise<void>((resolve) => server.close(() => resolve()));
}
});
it('returns 404 for run history on an unknown routine', async () => {
const { app } = buildApp();
const { server, port } = await listen(app);
try {
const res = await fetch(`http://127.0.0.1:${port}/api/routines/missing/runs?limit=10`);
expect(res.status).toBe(404);
const json = await res.json() as { error: string };
expect(json.error).toBe('routine not found');
} finally {
await new Promise<void>((resolve) => server.close(() => resolve()));
}
});
it('rejects invalid schedule input during routine creation', async () => {
const { app } = buildApp();
const { server, port } = await listen(app);
try {
const res = await fetch(`http://127.0.0.1:${port}/api/routines`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
name: 'Broken hourly digest',
prompt: 'Summarize activity.',
schedule: { kind: 'hourly', minute: 75 },
target: { mode: 'create_each_run' },
enabled: true,
}),
});
expect(res.status).toBe(400);
const json = await res.json() as { error: string };
expect(json.error).toContain('minute');
} finally {
await new Promise<void>((resolve) => server.close(() => resolve()));
}
});
});