Files
memind/scheduled-task-worker.test.mjs
T
john 7c0ed58ae1
Memind CI / Test, build, and release guards (push) Successful in 3m36s
fix(scheduled-task): release page delivery contracts before notifying users
Scheduled automation now prepares and retries MindSpace delivery contracts,
blocks WeChat pushes when public HTML links are not ready, and reconciles
stuck static preparing contracts on each worker scan.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-16 08:26:21 +08:00

292 lines
7.5 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { startScheduledTaskWorker } from './scheduled-task-worker.mjs';
test('scheduled task worker executes due task and completes one-shot task', async () => {
const calls = [];
const notifications = [];
const wechat = [];
const task = {
id: 'task-1',
userId: 'user-1',
title: '每日新闻页',
taskSpec: '做新闻页',
recurrence: 'once',
notifyChannel: 'both',
attempts: 1,
};
const worker = startScheduledTaskWorker({
intervalMs: 60_000,
userAuth: { id: 'user-auth' },
tkmindProxy: { id: 'proxy' },
scheduledTaskService: {
async listDueTasks() {
calls.push('list');
return [task];
},
async lockTask(id) {
calls.push(`lock:${id}`);
return task;
},
async markTaskRunning(input) {
calls.push(`running:${input.id}`);
return { ...input, status: 'running' };
},
async markTaskSucceeded(input, payload) {
calls.push(`success:${input.id}:${payload.deliveryText}`);
return { ...input, status: 'completed' };
},
async markTaskFailed() {
calls.push('failed');
},
},
scheduleService: {
async createUserNotification(input) {
notifications.push(input.notificationType);
},
},
notificationDispatcher: {
async sendScheduleNotification({ userId, text }) {
wechat.push({ userId, text });
},
},
executeTask: async () => ({
sessionId: 'session-1',
requestId: 'req-1',
deliveryText: '页面:https://example.com/news.html',
readyPaths: [],
}),
logger: { warn() {}, info() {} },
runOnStart: false,
});
await worker.runOnce();
worker.stop();
assert.deepEqual(calls, [
'list',
'lock:task-1',
'running:task-1',
'success:task-1:页面:https://example.com/news.html',
]);
assert.deepEqual(notifications, ['scheduled_task_result']);
assert.equal(wechat.length, 1);
assert.match(wechat[0].text, /定时任务完成:每日新闻页/);
});
test('scheduled task worker marks failure after execution error', async () => {
const calls = [];
const task = {
id: 'task-2',
userId: 'user-2',
title: '失败任务',
recurrence: 'daily',
notifyChannel: 'web',
attempts: 3,
};
const worker = startScheduledTaskWorker({
intervalMs: 60_000,
userAuth: { id: 'user-auth' },
tkmindProxy: { id: 'proxy' },
scheduledTaskService: {
async listDueTasks() {
return [task];
},
async lockTask() {
return task;
},
async markTaskRunning(input) {
return input;
},
async markTaskSucceeded() {
calls.push('success');
},
async markTaskFailed(input, err, { maxAttempts }) {
calls.push(`failed:${maxAttempts}:${err.message}`);
return { ...input, status: 'failed', lastError: err.message };
},
},
scheduleService: {
async createUserNotification(input) {
calls.push(`notify:${input.notificationType}`);
},
},
executeTask: async () => {
throw new Error('agent timeout');
},
maxAttempts: 3,
logger: { warn() {} },
runOnStart: false,
});
await worker.runOnce();
worker.stop();
assert.deepEqual(calls, ['failed:3:agent timeout', 'notify:scheduled_task_failed']);
});
test('scheduled task worker marks failure when agent only asks for clarification', async () => {
const calls = [];
const task = {
id: 'task-3',
userId: 'user-3',
title: '每日诗意清晨',
recurrence: 'daily',
notifyChannel: 'web',
attempts: 1,
};
const worker = startScheduledTaskWorker({
intervalMs: 60_000,
userAuth: { id: 'user-auth' },
tkmindProxy: { id: 'proxy' },
scheduledTaskService: {
async listDueTasks() {
return [task];
},
async lockTask() {
return task;
},
async markTaskRunning(input) {
return input;
},
async markTaskSucceeded() {
calls.push('success');
},
async markTaskFailed(input, err) {
calls.push(`failed:${err.code}`);
return { ...input, status: 'failed', lastError: err.message };
},
},
scheduleService: {
async createUserNotification(input) {
calls.push(`notify:${input.notificationType}`);
},
},
executeTask: async () => ({
sessionId: 'session-3',
requestId: 'req-3',
deliveryText: '请问每天早上几点执行?',
}),
logger: { warn() {} },
runOnStart: false,
});
await worker.runOnce();
worker.stop();
assert.deepEqual(calls, ['failed:SCHEDULED_TASK_NON_DELIVERY', 'notify:scheduled_task_failed']);
});
test('scheduled task worker keeps success when wechat notification fails', async () => {
const calls = [];
const task = {
id: 'task-4',
userId: 'user-4',
title: '每日新闻页',
recurrence: 'daily',
notifyChannel: 'both',
attempts: 1,
};
const worker = startScheduledTaskWorker({
intervalMs: 60_000,
userAuth: { id: 'user-auth' },
tkmindProxy: { id: 'proxy' },
scheduledTaskService: {
async listDueTasks() {
return [task];
},
async lockTask() {
return task;
},
async markTaskRunning(input) {
return input;
},
async markTaskSucceeded(input) {
calls.push(`success:${input.id}`);
return { ...input, status: 'completed' };
},
async markTaskFailed() {
calls.push('failed');
},
},
scheduleService: {
async createUserNotification() {
calls.push('notify:web');
},
},
notificationDispatcher: {
async sendScheduleNotification() {
const err = new Error('out of response count limit');
err.errcode = 45047;
throw err;
},
},
executeTask: async () => ({
sessionId: 'session-4',
requestId: 'req-4',
deliveryText: '页面已生成:https://example.com/news.html',
readyPaths: [],
}),
logger: { warn() {} },
runOnStart: false,
});
await worker.runOnce();
worker.stop();
assert.deepEqual(calls, ['notify:web', 'success:task-4']);
});
test('scheduled task worker marks failure when page link is not deliverable yet', async () => {
const calls = [];
const task = {
id: 'task-5',
userId: 'user-5',
title: '每日新闻页',
recurrence: 'daily',
notifyChannel: 'web',
attempts: 1,
};
const worker = startScheduledTaskWorker({
intervalMs: 60_000,
userAuth: { id: 'user-auth' },
tkmindProxy: { id: 'proxy' },
scheduledTaskService: {
async listDueTasks() {
return [task];
},
async lockTask() {
return task;
},
async markTaskRunning(input) {
return input;
},
async markTaskSucceeded() {
calls.push('success');
},
async markTaskFailed(input, err) {
calls.push(`failed:${err.code}`);
return { ...input, status: 'failed', lastError: err.message };
},
},
scheduleService: {
async createUserNotification(input) {
calls.push(`notify:${input.notificationType}`);
},
},
executeTask: async () => ({
sessionId: 'session-5',
requestId: 'req-5',
deliveryText: '页面:https://m.tkmind.cn/MindSpace/user-5/public/daily-news.html',
readyPaths: [],
}),
logger: { warn() {} },
runOnStart: false,
});
await worker.runOnce();
worker.stop();
assert.deepEqual(calls, ['failed:SCHEDULED_TASK_NON_DELIVERY', 'notify:scheduled_task_failed']);
});