1ca06e58e7
Memind CI / Test, build, and release guards (push) Successful in 8m42s
Wait for HTML materialization before releasing delivery contracts, pass verified MindSpace URLs through proactive WeChat sends, resend on reconcile when pages land late, and report deferred customer-service delivery as unsent. Co-authored-by: Cursor <cursoragent@cursor.com>
236 lines
8.0 KiB
JavaScript
236 lines
8.0 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs/promises';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import test from 'node:test';
|
|
import {
|
|
buildScheduledTaskExecutionPrompt,
|
|
buildScheduledTaskVerifiedHtmlUrls,
|
|
deliveryTextPromisesPublicHtml,
|
|
extractPublicHtmlPathsFromText,
|
|
extractScheduledTaskDeliveryText,
|
|
finalizeScheduledTaskPageDelivery,
|
|
formatScheduledTaskDeliveryMessage,
|
|
looksLikeScheduledTaskNonDelivery,
|
|
reconcileStuckStaticPageDeliveryContracts,
|
|
resolveScheduledTaskDeliveryPollIntervalMs,
|
|
resolveScheduledTaskDeliveryRetryDelaysMs,
|
|
} from './scheduled-task-executor.mjs';
|
|
|
|
test('buildScheduledTaskExecutionPrompt includes task spec and automation marker', () => {
|
|
const message = buildScheduledTaskExecutionPrompt({
|
|
id: 'task-1',
|
|
title: '每日新闻页',
|
|
taskSpec: '搜索今日新闻并生成 HTML 页面',
|
|
recurrence: 'daily',
|
|
timezone: 'Asia/Shanghai',
|
|
});
|
|
const text = message.content[0].text;
|
|
assert.match(text, /scheduled-task-automation/);
|
|
assert.match(text, /每日新闻页/);
|
|
assert.match(text, /搜索今日新闻并生成 HTML 页面/);
|
|
assert.match(text, /Scheduled Automation/);
|
|
assert.match(text, /禁止调用 scheduled_task_create/);
|
|
});
|
|
|
|
test('looksLikeScheduledTaskNonDelivery detects clarification replies', () => {
|
|
assert.equal(
|
|
looksLikeScheduledTaskNonDelivery('请问 7月31日具体几点执行这个任务?'),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
looksLikeScheduledTaskNonDelivery('页面已生成:https://example.com/news.html'),
|
|
false,
|
|
);
|
|
assert.equal(
|
|
looksLikeScheduledTaskNonDelivery(
|
|
'页面已生成:https://m.tkmind.cn/MindSpace/user-1/public/daily-news.html',
|
|
),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
looksLikeScheduledTaskNonDelivery('好的', { readyPaths: ['public/news.html'] }),
|
|
false,
|
|
);
|
|
assert.equal(looksLikeScheduledTaskNonDelivery('验证成功'), false);
|
|
assert.equal(looksLikeScheduledTaskNonDelivery('已完成'), false);
|
|
assert.equal(looksLikeScheduledTaskNonDelivery('好的'), true);
|
|
});
|
|
|
|
test('extractPublicHtmlPathsFromText parses MindSpace public links', () => {
|
|
assert.deepEqual(
|
|
extractPublicHtmlPathsFromText(
|
|
'链接:https://m.tkmind.cn/MindSpace/u/public/daily-news-0816.html',
|
|
),
|
|
['public/daily-news-0816.html'],
|
|
);
|
|
});
|
|
|
|
test('deliveryTextPromisesPublicHtml detects page delivery replies', () => {
|
|
assert.equal(
|
|
deliveryTextPromisesPublicHtml('今日摘要:天气不错'),
|
|
false,
|
|
);
|
|
assert.equal(
|
|
deliveryTextPromisesPublicHtml('public/daily-news.html 已生成'),
|
|
true,
|
|
);
|
|
});
|
|
|
|
test('resolveScheduledTaskDeliveryPollIntervalMs reads env override', () => {
|
|
assert.equal(
|
|
resolveScheduledTaskDeliveryPollIntervalMs({
|
|
H5_SCHEDULED_TASK_DELIVERY_POLL_INTERVAL_MS: '7500',
|
|
}),
|
|
7500,
|
|
);
|
|
});
|
|
|
|
test('resolveScheduledTaskDeliveryRetryDelaysMs keeps backward-compatible shape', () => {
|
|
assert.deepEqual(
|
|
resolveScheduledTaskDeliveryRetryDelaysMs({
|
|
H5_SCHEDULED_TASK_DELIVERY_POLL_INTERVAL_MS: '100',
|
|
}),
|
|
[0, 100, 100, 100],
|
|
);
|
|
});
|
|
|
|
test('buildScheduledTaskVerifiedHtmlUrls maps ready paths to public urls', () => {
|
|
assert.deepEqual(
|
|
buildScheduledTaskVerifiedHtmlUrls('user-1', ['public/news.html'], {
|
|
publicBaseUrl: 'https://m.tkmind.cn',
|
|
}),
|
|
['https://m.tkmind.cn/MindSpace/user-1/public/news.html'],
|
|
);
|
|
});
|
|
|
|
test('finalizeScheduledTaskPageDelivery prepares and releases static contracts', async () => {
|
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'scheduled-task-finalize-'));
|
|
const publishDir = path.join(dir, 'MindSpace', 'user-1');
|
|
const relativePath = 'public/news.html';
|
|
await fs.mkdir(path.dirname(path.join(publishDir, relativePath)), { recursive: true });
|
|
await fs.writeFile(path.join(publishDir, relativePath), '<html></html>', 'utf8');
|
|
const calls = [];
|
|
const pool = {
|
|
async query(sql, params) {
|
|
calls.push({ sql, params });
|
|
if (sql.includes('FROM h5_page_delivery_contracts')) {
|
|
return [[{ workspace_relative_path: relativePath }]];
|
|
}
|
|
if (sql.includes('SELECT id, data_mode, status')) {
|
|
return [[{ id: 'c1', data_mode: 'static', status: 'preparing' }]];
|
|
}
|
|
if (sql.includes("SET status = 'ready'")) {
|
|
return [{ affectedRows: 1 }];
|
|
}
|
|
return [[]];
|
|
},
|
|
};
|
|
const readyPaths = await finalizeScheduledTaskPageDelivery({
|
|
pool,
|
|
userId: 'user-1',
|
|
sessionId: 'session-1',
|
|
messages: [],
|
|
publishDir,
|
|
deliveryText: 'public/news.html 已生成',
|
|
logger: { warn() {}, info() {} },
|
|
});
|
|
assert.deepEqual(readyPaths, [relativePath]);
|
|
assert.ok(calls.some((call) => call.sql.includes('INSERT INTO h5_page_delivery_contracts')));
|
|
});
|
|
|
|
test('finalizeScheduledTaskPageDelivery skips release when html file is missing', async () => {
|
|
const publishDir = path.join(await fs.mkdtemp(path.join(os.tmpdir(), 'scheduled-task-finalize-missing-')), 'MindSpace', 'user-1');
|
|
await fs.mkdir(publishDir, { recursive: true });
|
|
const pool = {
|
|
async query(sql) {
|
|
if (sql.includes('FROM h5_page_delivery_contracts')) {
|
|
return [[{ workspace_relative_path: 'public/news.html' }]];
|
|
}
|
|
if (sql.includes('SELECT id, data_mode, status')) {
|
|
return [[{ id: 'c1', data_mode: 'static', status: 'preparing' }]];
|
|
}
|
|
if (sql.includes("SET status = 'ready'")) {
|
|
throw new Error('should not mark ready without html file');
|
|
}
|
|
return [[]];
|
|
},
|
|
};
|
|
const readyPaths = await finalizeScheduledTaskPageDelivery({
|
|
pool,
|
|
userId: 'user-1',
|
|
sessionId: 'session-1',
|
|
messages: [],
|
|
publishDir,
|
|
deliveryText: 'public/news.html 已生成',
|
|
logger: { warn() {}, info() {} },
|
|
});
|
|
assert.deepEqual(readyPaths, []);
|
|
});
|
|
|
|
test('reconcileStuckStaticPageDeliveryContracts releases materialized static pages', async () => {
|
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'scheduled-task-'));
|
|
const userId = 'user-1';
|
|
const relativePath = 'public/news.html';
|
|
const filePath = path.join(dir, 'MindSpace', userId, relativePath);
|
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
await fs.writeFile(filePath, '<html></html>', 'utf8');
|
|
const pool = {
|
|
async query(sql) {
|
|
if (sql.includes('FROM h5_page_delivery_contracts')) {
|
|
return [[{ user_id: userId, workspace_relative_path: relativePath }]];
|
|
}
|
|
if (sql.includes("SET status = 'ready'")) {
|
|
return [{ affectedRows: 1 }];
|
|
}
|
|
return [[]];
|
|
},
|
|
};
|
|
const released = await reconcileStuckStaticPageDeliveryContracts({
|
|
pool,
|
|
h5Root: dir,
|
|
logger: { info() {} },
|
|
});
|
|
assert.deepEqual(released, [{ userId, relativePath }]);
|
|
});
|
|
|
|
test('reconcileStuckStaticPageDeliveryContracts fails orphan contracts without html', async () => {
|
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'scheduled-task-orphan-'));
|
|
const userId = 'user-1';
|
|
const relativePath = 'public/missing.html';
|
|
const pool = {
|
|
async query(sql) {
|
|
if (sql.includes('FROM h5_page_delivery_contracts')) {
|
|
return [[{ user_id: userId, workspace_relative_path: relativePath }]];
|
|
}
|
|
if (sql.includes("SET status = 'failed'")) {
|
|
return [{ affectedRows: 1 }];
|
|
}
|
|
return [[]];
|
|
},
|
|
};
|
|
const released = await reconcileStuckStaticPageDeliveryContracts({
|
|
pool,
|
|
h5Root: dir,
|
|
logger: { info() {} },
|
|
});
|
|
assert.deepEqual(released, []);
|
|
});
|
|
|
|
test('extractScheduledTaskDeliveryText reads last assistant message', () => {
|
|
const text = extractScheduledTaskDeliveryText([
|
|
{ role: 'user', content: [{ type: 'text', text: 'hi' }] },
|
|
{ role: 'assistant', content: [{ type: 'text', text: '今日新闻页:https://example.com/news.html' }] },
|
|
], { title: '每日新闻页' });
|
|
assert.match(text, /https:\/\/example.com\/news.html/);
|
|
});
|
|
|
|
test('formatScheduledTaskDeliveryMessage wraps delivery body', () => {
|
|
const text = formatScheduledTaskDeliveryMessage(
|
|
{ title: '每日新闻页' },
|
|
'页面已生成:https://example.com/news.html',
|
|
);
|
|
assert.match(text, /定时任务完成:每日新闻页/);
|
|
assert.match(text, /页面已生成/);
|
|
});
|