Files
memind/scheduled-task-service.test.mjs
T
john a1a921eba6 fix(wechat): route scheduled task cancel and spec updates through Goose MCP
Pass cancel requests to Goose like create instead of inbound title matching,
and add scheduled_task_update_spec so format or execution constraints update
taskSpec without triggering page delivery guards.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-29 09:47:39 +08:00

342 lines
9.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from 'node:assert/strict';
import test from 'node:test';
import { createScheduledTaskService, computeScheduledTaskNextRunAt } from './scheduled-task-service.mjs';
import { nextDailyRunAt } from './schedule-time.mjs';
test('computeScheduledTaskNextRunAt supports daily schedule', () => {
const now = Date.parse('2026-07-31T02:00:00+08:00');
const nextRunAt = computeScheduledTaskNextRunAt({
recurrence: 'daily',
hour: 6,
minute: 0,
timezone: 'Asia/Shanghai',
now,
});
assert.equal(nextRunAt, nextDailyRunAt({ hour: 6, minute: 0, timezone: 'Asia/Shanghai', now }));
});
test('createTask inserts scheduled automation row', async () => {
const inserts = [];
const service = createScheduledTaskService({
async query(sql, params) {
if (sql.includes('FROM h5_scheduled_tasks') && sql.includes('status = \'active\'')) {
return [[]];
}
if (sql.includes('INSERT INTO h5_scheduled_tasks')) {
inserts.push(params);
return [{ affectedRows: 1 }];
}
if (sql.includes('SELECT * FROM h5_scheduled_tasks WHERE id = ?')) {
return [[{
id: params[0],
user_id: 'user-1',
title: '每日新闻页',
task_spec: '搜索今日新闻并生成 HTML 页面',
recurrence: 'daily',
hour: 6,
minute: 0,
weekday: null,
timezone: 'Asia/Shanghai',
next_run_at: 1780000000000,
last_run_at: null,
notify_channel: 'both',
status: 'active',
attempts: 0,
last_error: null,
last_result_json: null,
source_channel: 'agent',
source_session_id: null,
source_message_id: null,
source_text: null,
created_at: 1780000000000,
updated_at: 1780000000000,
}]];
}
return [[]];
},
}, {
clock: { now: () => 1780000000000 },
});
const task = await service.createTask({
userId: 'user-1',
title: '每日新闻页',
taskSpec: '搜索今日新闻并生成 HTML 页面',
recurrence: 'daily',
hour: 6,
minute: 0,
});
assert.equal(task.title, '每日新闻页');
assert.equal(task.recurrence, 'daily');
assert.equal(inserts.length, 1);
});
test('createTask returns existing active duplicate for same schedule', async () => {
let insertCount = 0;
const service = createScheduledTaskService({
async query(sql) {
if (sql.includes('FROM h5_scheduled_tasks') && sql.includes("status IN ('active', 'locked')")) {
return [[{
id: 'task-existing',
user_id: 'user-1',
title: '每日天气预报播报(上海+武穴)',
task_spec: 'old spec',
recurrence: 'daily',
hour: 8,
minute: 0,
weekday: null,
timezone: 'Asia/Shanghai',
next_run_at: 1780000000000,
last_run_at: null,
notify_channel: 'both',
status: 'active',
attempts: 0,
last_error: null,
last_result_json: null,
source_channel: 'agent',
source_session_id: null,
source_message_id: null,
source_text: null,
created_at: 1780000000000,
updated_at: 1780000000000,
}]];
}
if (sql.includes('INSERT INTO h5_scheduled_tasks')) {
insertCount += 1;
return [{ affectedRows: 1 }];
}
return [[]];
},
}, {
clock: { now: () => 1780000000000 },
});
const task = await service.createTask({
userId: 'user-1',
title: '每日天气预报播报(上海+武穴)',
taskSpec: 'new spec',
recurrence: 'daily',
hour: 8,
minute: 0,
});
assert.equal(task.id, 'task-existing');
assert.equal(insertCount, 0);
});
test('createTask returns existing locked duplicate for same schedule', async () => {
let insertCount = 0;
const service = createScheduledTaskService({
async query(sql) {
if (sql.includes('FROM h5_scheduled_tasks') && sql.includes("status IN ('active', 'locked')")) {
return [[{
id: 'task-locked',
user_id: 'user-1',
title: '每日天气预报播报(上海+武穴)',
task_spec: 'running spec',
recurrence: 'daily',
hour: 8,
minute: 0,
weekday: null,
timezone: 'Asia/Shanghai',
next_run_at: 1780000000000,
last_run_at: null,
notify_channel: 'both',
status: 'locked',
attempts: 1,
last_error: null,
last_result_json: null,
source_channel: 'agent',
source_session_id: null,
source_message_id: null,
source_text: null,
created_at: 1780000000000,
updated_at: 1780000000000,
}]];
}
if (sql.includes('INSERT INTO h5_scheduled_tasks')) {
insertCount += 1;
return [{ affectedRows: 1 }];
}
return [[]];
},
}, {
clock: { now: () => 1780000000000 },
});
const task = await service.createTask({
userId: 'user-1',
title: '每日天气预报播报(上海+武穴)',
taskSpec: 'new spec',
recurrence: 'daily',
hour: 8,
minute: 0,
});
assert.equal(task.id, 'task-locked');
assert.equal(insertCount, 0);
});
test('cancelTask updates status to cancelled', async () => {
const service = createScheduledTaskService({
async query(sql, params) {
if (sql.includes('UPDATE h5_scheduled_tasks') && sql.includes('cancelled')) {
return [{ affectedRows: 1 }];
}
if (sql.includes('SELECT * FROM h5_scheduled_tasks WHERE id = ?')) {
return [[{
id: params[0],
user_id: 'user-1',
title: '每日新闻页',
task_spec: '搜索今日新闻并生成 HTML 页面',
recurrence: 'daily',
hour: 6,
minute: 0,
weekday: null,
timezone: 'Asia/Shanghai',
next_run_at: 1780000000000,
last_run_at: null,
notify_channel: 'both',
status: 'cancelled',
attempts: 0,
last_error: null,
last_result_json: null,
source_channel: 'agent',
source_session_id: null,
source_message_id: null,
source_text: null,
created_at: 1780000000000,
updated_at: 1780000001000,
}]];
}
return [[]];
},
});
const task = await service.cancelTask({ userId: 'user-1', taskId: 'task-1' });
assert.equal(task.status, 'cancelled');
});
test('markTaskSucceeded completes one-shot task', async () => {
const updates = [];
const service = createScheduledTaskService({
async query(sql, params) {
if (sql.includes('UPDATE h5_scheduled_tasks') && sql.includes("'completed'")) {
updates.push(['completed', params]);
}
return [{ affectedRows: 1 }];
},
}, {
clock: { now: () => 1780000001000 },
});
const result = await service.markTaskSucceeded({
id: 'task-1',
recurrence: 'once',
title: '一次性任务',
nextRunAt: 1780000000000,
}, {
deliveryText: 'done',
sessionId: 'session-1',
requestId: 'req-1',
});
assert.equal(result.status, 'completed');
assert.equal(updates.length, 1);
});
test('markTaskSucceeded schedules next run for daily task', async () => {
const updates = [];
const service = createScheduledTaskService({
async query(sql, params) {
if (sql.includes('UPDATE h5_scheduled_tasks') && sql.includes("'active'")) {
updates.push(params);
}
return [{ affectedRows: 1 }];
},
}, {
clock: { now: () => Date.parse('2026-07-31T02:00:00+08:00') },
});
const result = await service.markTaskSucceeded({
id: 'task-2',
recurrence: 'daily',
hour: 6,
minute: 0,
timezone: 'Asia/Shanghai',
title: '每日任务',
nextRunAt: Date.parse('2026-07-31T06:00:00+08:00'),
}, {
deliveryText: 'done',
});
assert.equal(result.status, 'active');
assert.ok(result.nextRunAt > Date.parse('2026-07-31T02:00:00+08:00'));
assert.equal(updates.length, 1);
});
test('updateTaskSpec replaces or merges taskSpec', async () => {
const activeTask = {
id: 'task-news',
user_id: 'user-1',
title: '每日新闻早报页面(5:30',
task_spec: '按旧格式生成新闻页',
recurrence: 'daily',
hour: 5,
minute: 30,
weekday: null,
timezone: 'Asia/Shanghai',
next_run_at: 1780000000000,
last_run_at: null,
notify_channel: 'both',
status: 'active',
attempts: 0,
last_error: null,
last_result_json: null,
source_channel: 'agent',
source_session_id: null,
source_message_id: null,
source_text: null,
created_at: 1780000000000,
updated_at: 1780000000000,
};
let stored = { ...activeTask };
const service = createScheduledTaskService({
async query(sql, params) {
if (sql.includes('FROM h5_scheduled_tasks') && sql.includes('status IN')) {
return [[stored]];
}
if (sql.includes('UPDATE h5_scheduled_tasks') && sql.includes('task_spec')) {
stored = {
...stored,
task_spec: params[0],
title: params[1],
updated_at: params[2],
};
return [{ affectedRows: 1 }];
}
if (sql.includes('SELECT * FROM h5_scheduled_tasks WHERE id = ? LIMIT 1')) {
return [[stored]];
}
return [[]];
},
});
const merged = await service.updateTaskSpec({
userId: 'user-1',
taskId: 'task-news',
taskSpec: '参照 daily-news-0829.html 作为标准格式',
mergeTaskSpec: true,
});
assert.match(merged.taskSpec, /旧格式/u);
assert.match(merged.taskSpec, /daily-news-0829/u);
const replaced = await service.updateTaskSpec({
userId: 'user-1',
taskId: 'task-news',
taskSpec: '全新执行说明',
});
assert.equal(replaced.taskSpec, '全新执行说明');
});