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), '', '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, '', '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, /页面已生成/); });