843e4d1002
Memind CI / Test, build, and release guards (push) Successful in 2m58s
Prevent duplicate active scheduled tasks, fail static preparing contracts when HTML is missing, raise MindSpace remote timeout default to 30s, and add 103 repair scripts for inspection follow-ups. Co-authored-by: Cursor <cursoragent@cursor.com>
225 lines
6.5 KiB
JavaScript
225 lines
6.5 KiB
JavaScript
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 = \'active\'')) {
|
|
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('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);
|
|
});
|