Files
memind/wechat-news-morning-draft-worker.test.mjs
T
john 57a9aadf53
Memind CI / Test, build, and release guards (push) Successful in 4m43s
fix(news): repair instead of block in daily news generation and push
- news002 image/structure audit no longer throws: prune empty sections and
  publish a best-effort page with a warning
- WeChat push strips images that failed to upload instead of refusing the draft
- forceGenerateOnce is cleared before running, so a failed run cannot loop;
  the archived page is restored when generation fails
- degraded-page regeneration runs at most once per day
- content safety rules cover vulgar sexual terms and gossip/adult sites

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-23 11:29:35 +08:00

544 lines
19 KiB
JavaScript

import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { PUBLIC_ZONE_DIR, PUBLISH_ROOT_DIR } from './user-publish.mjs';
import { startWechatNewsMorningDraftWorker } from './wechat-news-morning-draft-worker.mjs';
import {
createWechatNewsMorningDraftService,
wechatNewsMorningDraftInternals,
} from './wechat-news-morning-draft.mjs';
const SAMPLE_HTML = `<!DOCTYPE html><html><body><h1>📰 每日新闻早报</h1></body></html>`;
const HEALTHY_TODAY_HTML = `${SAMPLE_HTML}${'<div class="card"><h3>条目</h3></div>'.repeat(8)}`;
function createPool() {
const rows = [];
return {
rows,
async query(sql, params = []) {
if (sql.includes('CREATE TABLE')) return [[], []];
if (sql.includes('FROM h5_wechat_admin_config')) return [[], []];
if (sql.includes('INSERT INTO h5_wechat_news_draft_runs')) {
rows.push({
id: params[0],
status: params[1],
pageSlug: params[2],
pageUrl: params[3],
draftMediaId: params[4],
errorMessage: params[5],
triggeredBy: params[6],
createdAt: Number(params[7]),
});
return [[], []];
}
if (sql.includes('FROM h5_wechat_news_draft_runs')) {
return [rows.slice().reverse(), []];
}
return [[], []];
},
};
}
test('news morning draft worker auto-pushes at configured time', async () => {
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'news-draft-worker-'));
const userId = 'user-news';
const publicDir = path.join(tmpRoot, PUBLISH_ROOT_DIR, userId, PUBLIC_ZONE_DIR);
fs.mkdirSync(publicDir, { recursive: true });
fs.writeFileSync(path.join(publicDir, 'daily-news-0911.html'), SAMPLE_HTML);
const pool = createPool();
const service = createWechatNewsMorningDraftService(pool, {
mpConfig: { enabled: true, appId: 'app', appSecret: 'secret' },
h5Root: tmpRoot,
env: {
H5_WECHAT_NEWS_MORNING_DRAFT_ENABLED: '1',
H5_WECHAT_NEWS_MORNING_DRAFT_AUTO: '1',
H5_WECHAT_NEWS_MORNING_DRAFT_USER_ID: userId,
H5_WECHAT_NEWS_MORNING_DRAFT_HOUR: '6',
H5_WECHAT_NEWS_MORNING_DRAFT_MINUTE: '0',
},
});
const pushCalls = [];
service.pushDraft = async (options = {}) => {
pushCalls.push(options);
return {
ok: true,
draftMediaId: 'draft-1',
preview: { page: { slug: 'daily-news-0911' } },
};
};
service.hasSuccessfulAutoPushToday = async () => false;
const fixedNow = Date.UTC(2026, 8, 10, 22, 0, 0);
const originalNow = Date.now;
Date.now = () => fixedNow;
try {
const worker = startWechatNewsMorningDraftWorker({
wechatNewsMorningDraftService: service,
mpConfig: { enabled: true },
userAuth: { canUseChat: async () => ({ ok: true }) },
tkmindProxy: { id: 'proxy' },
h5Root: tmpRoot,
env: {
H5_WECHAT_NEWS_MORNING_DRAFT_WORKER_ENABLED: '1',
},
intervalMs: 30_000,
runOnStart: false,
setIntervalFn: () => ({ unref() {} }),
});
await worker.runOnce();
assert.equal(pushCalls.length, 1);
assert.equal(pushCalls[0].triggeredBy, 'auto-push');
} finally {
Date.now = originalNow;
fs.rmSync(tmpRoot, { recursive: true, force: true });
}
});
test('runOnce auto-generates in the lead window even if auto-push is off', async () => {
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'news-draft-autogen-'));
const userId = 'user-news';
const publicDir = path.join(tmpRoot, PUBLISH_ROOT_DIR, userId, PUBLIC_ZONE_DIR);
fs.mkdirSync(publicDir, { recursive: true });
const pool = createPool();
const service = createWechatNewsMorningDraftService(pool, {
mpConfig: { enabled: true, appId: 'app', appSecret: 'secret' },
h5Root: tmpRoot,
env: {
H5_WECHAT_NEWS_MORNING_DRAFT_ENABLED: '1',
H5_WECHAT_NEWS_MORNING_DRAFT_USER_ID: userId,
},
});
service.getConfig = async () => ({
enabled: true,
sourceUserId: userId,
autoPushEnabled: false,
autoGenerateEnabled: true,
generateLeadMinutes: 60,
timezone: 'Asia/Shanghai',
pushHour: 6,
pushMinute: 0,
pageSlugPattern: 'daily-news-*',
});
const pushCalls = [];
service.pushDraft = async (options = {}) => {
pushCalls.push(options);
return { ok: true, draftMediaId: 'draft-should-not-fire' };
};
const executeCalls = [];
const fixedNow = Date.UTC(2026, 8, 10, 21, 15, 0);
const originalNow = Date.now;
Date.now = () => fixedNow;
try {
const worker = startWechatNewsMorningDraftWorker({
wechatNewsMorningDraftService: service,
mpConfig: { enabled: true },
userAuth: { canUseChat: async () => ({ ok: true }) },
tkmindProxy: { id: 'proxy' },
h5Root: tmpRoot,
env: { H5_WECHAT_NEWS_MORNING_DRAFT_WORKER_ENABLED: '1' },
executeTask: async () => {
executeCalls.push('generate');
fs.writeFileSync(path.join(publicDir, 'daily-news-0911.html'), `${SAMPLE_HTML}<!--lead-->`);
},
intervalMs: 30_000,
runOnStart: false,
setIntervalFn: () => ({ unref() {} }),
});
await worker.runOnce();
assert.equal(executeCalls.length, 1);
assert.equal(pushCalls.length, 0);
assert.match(fs.readFileSync(path.join(publicDir, 'daily-news-0911.html'), 'utf8'), /lead/);
} finally {
Date.now = originalNow;
fs.rmSync(tmpRoot, { recursive: true, force: true });
}
});
test('runOnce honors forceGenerateOnce even when today page exists', async () => {
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'news-draft-flag-'));
const userId = 'user-news';
const publicDir = path.join(tmpRoot, PUBLISH_ROOT_DIR, userId, PUBLIC_ZONE_DIR);
fs.mkdirSync(publicDir, { recursive: true });
fs.writeFileSync(path.join(publicDir, 'daily-news-0911.html'), HEALTHY_TODAY_HTML);
const pool = createPool();
const service = createWechatNewsMorningDraftService(pool, {
mpConfig: { enabled: true, appId: 'app', appSecret: 'secret' },
h5Root: tmpRoot,
env: {
H5_WECHAT_NEWS_MORNING_DRAFT_ENABLED: '1',
H5_WECHAT_NEWS_MORNING_DRAFT_USER_ID: userId,
},
});
let config = {
enabled: true,
sourceUserId: userId,
autoPushEnabled: true,
autoGenerateEnabled: true,
forceGenerateOnce: true,
timezone: 'Asia/Shanghai',
pushHour: 6,
pushMinute: 0,
pageSlugPattern: 'daily-news-*',
};
service.getConfig = async () => config;
service.updateConfig = async (payload) => {
config = { ...config, ...payload };
return config;
};
const pushCalls = [];
service.pushDraft = async (options = {}) => {
pushCalls.push(options);
return {
ok: true,
draftMediaId: 'draft-force',
preview: { page: { slug: 'daily-news-0911' } },
};
};
const executeCalls = [];
const fixedNow = Date.UTC(2026, 8, 10, 22, 30, 0);
const originalNow = Date.now;
Date.now = () => fixedNow;
try {
const worker = startWechatNewsMorningDraftWorker({
wechatNewsMorningDraftService: service,
mpConfig: { enabled: true },
userAuth: { canUseChat: async () => ({ ok: true }) },
tkmindProxy: { id: 'proxy' },
h5Root: tmpRoot,
env: { H5_WECHAT_NEWS_MORNING_DRAFT_WORKER_ENABLED: '1' },
executeTask: async (task) => {
executeCalls.push(task.taskSpec);
fs.writeFileSync(path.join(publicDir, 'daily-news-0911.html'), `${SAMPLE_HTML}<!--forced-->`);
},
intervalMs: 30_000,
runOnStart: false,
setIntervalFn: () => ({ unref() {} }),
});
await worker.runOnce();
assert.equal(executeCalls.length, 1);
assert.match(executeCalls[0], /禁止复制/u);
assert.equal(config.forceGenerateOnce, false);
assert.equal(pushCalls.length, 1);
assert.equal(pushCalls[0].triggeredBy, 'manual-generate');
assert.equal(fs.existsSync(path.join(publicDir, 'daily-news-0911.html')), true);
assert.ok(
fs.readdirSync(publicDir).some((name) => name.startsWith('_archived-daily-news-0911-')),
);
} finally {
Date.now = originalNow;
fs.rmSync(tmpRoot, { recursive: true, force: true });
}
});
test('failed forceGenerateOnce clears the flag, restores the original page and does not retry', async () => {
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'news-draft-force-fail-'));
const userId = 'user-news';
const publicDir = path.join(tmpRoot, PUBLISH_ROOT_DIR, userId, PUBLIC_ZONE_DIR);
fs.mkdirSync(publicDir, { recursive: true });
const pagePath = path.join(publicDir, 'daily-news-0911.html');
fs.writeFileSync(pagePath, HEALTHY_TODAY_HTML);
const service = createWechatNewsMorningDraftService(createPool(), {
mpConfig: { enabled: true, appId: 'app', appSecret: 'secret' },
h5Root: tmpRoot,
env: {
H5_WECHAT_NEWS_MORNING_DRAFT_ENABLED: '1',
H5_WECHAT_NEWS_MORNING_DRAFT_USER_ID: userId,
},
});
let config = {
enabled: true,
sourceUserId: userId,
autoPushEnabled: true,
autoGenerateEnabled: true,
forceGenerateOnce: true,
timezone: 'Asia/Shanghai',
pushHour: 6,
pushMinute: 0,
pageSlugPattern: 'daily-news-*',
};
service.getConfig = async () => config;
service.updateConfig = async (payload) => {
config = { ...config, ...payload };
return config;
};
const pushCalls = [];
service.pushDraft = async (options = {}) => {
pushCalls.push(options);
return { ok: true, draftMediaId: 'draft-x' };
};
service.hasSuccessfulAutoPushToday = async () => true;
let executeCount = 0;
const fixedNow = Date.UTC(2026, 8, 10, 22, 30, 0);
const originalNow = Date.now;
Date.now = () => fixedNow;
try {
const worker = startWechatNewsMorningDraftWorker({
wechatNewsMorningDraftService: service,
mpConfig: { enabled: true },
userAuth: { canUseChat: async () => ({ ok: true }) },
tkmindProxy: { id: 'proxy' },
h5Root: tmpRoot,
env: { H5_WECHAT_NEWS_MORNING_DRAFT_WORKER_ENABLED: '1' },
executeTask: async () => {
executeCount += 1;
throw new Error('agent timeout');
},
prefetchNewsSearch: async () => ({ brief: '', groups: [] }),
logger: { log() {}, warn() {} },
intervalMs: 30_000,
runOnStart: false,
setIntervalFn: () => ({ unref() {} }),
});
await worker.runOnce();
await worker.runOnce();
assert.equal(executeCount, 1);
assert.equal(config.forceGenerateOnce, false);
assert.equal(pushCalls.length, 0);
assert.equal(fs.readFileSync(pagePath, 'utf8'), HEALTHY_TODAY_HTML);
assert.equal(
fs.readdirSync(publicDir).some((name) => name.startsWith('_archived-daily-news-0911-')),
false,
);
} finally {
Date.now = originalNow;
fs.rmSync(tmpRoot, { recursive: true, force: true });
}
});
test('news002 page that misses structure checks is still published with empty sections pruned', async () => {
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'news-draft-news002-degraded-'));
const userId = 'user-news';
const publicDir = path.join(tmpRoot, PUBLISH_ROOT_DIR, userId, PUBLIC_ZONE_DIR);
fs.mkdirSync(publicDir, { recursive: true });
const service = createWechatNewsMorningDraftService(createPool(), {
mpConfig: { enabled: true, appId: 'app', appSecret: 'secret' },
h5Root: tmpRoot,
env: {
H5_WECHAT_NEWS_MORNING_DRAFT_ENABLED: '1',
H5_WECHAT_NEWS_MORNING_DRAFT_USER_ID: userId,
H5_WECHAT_NEWS_MORNING_TEMPLATE_ID: 'news002',
},
});
const warnings = [];
const html = '<!DOCTYPE html><html><body><div class="masthead"><h1>每日新闻早报</h1></div>'
+ '<div class="section" id="domestic"><h2>国内</h2>'
+ '<div class="card" data-event-key="c919"><h3>国产大飞机交付第100架</h3>'
+ '<div class="body"><p>商飞公布最新交付数据。</p></div>'
+ '<a href="https://www.news.cn/a/20260911/c919.html">来源</a></div></div>'
+ '<div class="section" id="voices"><h2>声音</h2></div>'
+ '</body></html>';
const worker = startWechatNewsMorningDraftWorker({
wechatNewsMorningDraftService: service,
mpConfig: { enabled: true },
userAuth: { canUseChat: async () => ({ ok: true }) },
tkmindProxy: { id: 'proxy' },
h5Root: tmpRoot,
env: {
H5_WECHAT_NEWS_MORNING_DRAFT_WORKER_ENABLED: '1',
MEMIND_NEWS_MORNING_SEARXNG_FALLBACK: '0',
},
executeTask: async () => {
fs.writeFileSync(path.join(publicDir, 'daily-news-0911.html'), html);
},
prefetchNewsSearch: async () => ({ brief: '', groups: [] }),
contentReviewer: async () => ({ ok: true, violations: [] }),
logger: { log() {}, warn: (...args) => warnings.push(args) },
intervalMs: 30_000,
runOnStart: false,
setIntervalFn: () => ({ unref() {} }),
});
try {
const result = await worker.generateToday({ now: Date.UTC(2026, 8, 10, 22, 0, 0), force: true });
assert.equal(result.skipped, false);
const saved = fs.readFileSync(path.join(publicDir, 'daily-news-0911.html'), 'utf8');
assert.match(saved, /国产大飞机交付第100架/u);
assert.doesNotMatch(saved, /id="voices"/);
assert.ok(warnings.some(([message]) => /publishing best-effort page/.test(String(message))));
} finally {
fs.rmSync(tmpRoot, { recursive: true, force: true });
}
});
test('generateToday skips existing page unless force is set', async () => {
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'news-draft-force-'));
const userId = 'user-news';
const publicDir = path.join(tmpRoot, PUBLISH_ROOT_DIR, userId, PUBLIC_ZONE_DIR);
fs.mkdirSync(publicDir, { recursive: true });
fs.writeFileSync(path.join(publicDir, 'daily-news-0911.html'), SAMPLE_HTML);
const pool = createPool();
const service = createWechatNewsMorningDraftService(pool, {
mpConfig: { enabled: true, appId: 'app', appSecret: 'secret' },
h5Root: tmpRoot,
env: {
H5_WECHAT_NEWS_MORNING_DRAFT_ENABLED: '1',
H5_WECHAT_NEWS_MORNING_DRAFT_AUTO: '1',
H5_WECHAT_NEWS_MORNING_DRAFT_USER_ID: userId,
H5_WECHAT_NEWS_MORNING_TEMPLATE_ID: 'news001',
},
});
const executeCalls = [];
const fixedNow = Date.UTC(2026, 8, 10, 22, 0, 0);
const worker = startWechatNewsMorningDraftWorker({
wechatNewsMorningDraftService: service,
mpConfig: { enabled: true },
userAuth: { canUseChat: async () => ({ ok: true }) },
tkmindProxy: { id: 'proxy' },
h5Root: tmpRoot,
env: { H5_WECHAT_NEWS_MORNING_DRAFT_WORKER_ENABLED: '1' },
executeTask: async (task) => {
executeCalls.push(task.taskSpec);
fs.writeFileSync(path.join(publicDir, 'daily-news-0911.html'), `${SAMPLE_HTML}<!--forced-->`);
},
prefetchNewsSearch: async () => ({
brief: '【专用联网搜索预取】国内国际热门事件',
groups: [],
}),
intervalMs: 30_000,
runOnStart: false,
setIntervalFn: () => ({ unref() {} }),
});
try {
const skipped = await worker.generateToday({ now: fixedNow, force: false });
assert.equal(skipped.skipped, true);
assert.equal(executeCalls.length, 0);
const forced = await worker.generateToday({ now: fixedNow, force: true });
assert.equal(forced.skipped, false);
assert.equal(forced.page.slug, 'daily-news-0911');
assert.equal(executeCalls.length, 1);
assert.match(executeCalls[0], /禁止复制/u);
assert.match(executeCalls[0], /tkmind_search/u);
assert.match(executeCalls[0], /专用联网搜索预取/u);
assert.match(executeCalls[0], /国内国际热门事件/u);
assert.match(fs.readFileSync(path.join(publicDir, 'daily-news-0911.html'), 'utf8'), /forced/);
assert.ok(
fs.readdirSync(publicDir).some((name) => name.startsWith('_archived-daily-news-0911-')),
);
} finally {
fs.rmSync(tmpRoot, { recursive: true, force: true });
}
});
test('generateToday runs the content safety gate on the generated page without blocking it', async () => {
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'news-draft-safety-'));
const userId = 'user-news';
const publicDir = path.join(tmpRoot, PUBLISH_ROOT_DIR, userId, PUBLIC_ZONE_DIR);
fs.mkdirSync(publicDir, { recursive: true });
const service = createWechatNewsMorningDraftService(createPool(), {
mpConfig: { enabled: true, appId: 'app', appSecret: 'secret' },
h5Root: tmpRoot,
env: {
H5_WECHAT_NEWS_MORNING_DRAFT_ENABLED: '1',
H5_WECHAT_NEWS_MORNING_DRAFT_AUTO: '1',
H5_WECHAT_NEWS_MORNING_DRAFT_USER_ID: userId,
H5_WECHAT_NEWS_MORNING_TEMPLATE_ID: 'news001',
},
});
const unsafeCard = '<div class="card"><h3>91每日大赛-吃瓜爆料平台,明星绯闻每日更新</h3><p>黑料</p>'
+ '<div class="source"><a href="https://example.com/2026/09/11/gossip-feed">来源</a></div></div>'
+ '<div class="card"><h3>国产大飞机交付第100架</h3><p>商飞公布最新交付数据。</p>'
+ '<div class="source"><a href="https://www.news.cn/a/20260911/c919.html">来源</a></div></div>';
const reviewerCalls = [];
const worker = startWechatNewsMorningDraftWorker({
wechatNewsMorningDraftService: service,
mpConfig: { enabled: true },
userAuth: { canUseChat: async () => ({ ok: true }) },
tkmindProxy: { id: 'proxy' },
h5Root: tmpRoot,
env: { H5_WECHAT_NEWS_MORNING_DRAFT_WORKER_ENABLED: '1' },
executeTask: async (task) => {
assert.match(task.taskSpec, /内容安全底线/u);
fs.writeFileSync(
path.join(publicDir, 'daily-news-0911.html'),
SAMPLE_HTML.replace('</body>', `${unsafeCard}</body>`),
);
},
prefetchNewsSearch: async () => ({ brief: '', groups: [] }),
contentReviewer: async (items) => {
reviewerCalls.push(items.length);
return { ok: true, violations: [] };
},
logger: { log() {}, warn() {} },
intervalMs: 30_000,
runOnStart: false,
setIntervalFn: () => ({ unref() {} }),
});
try {
const result = await worker.generateToday({ now: Date.UTC(2026, 8, 10, 22, 0, 0), force: true });
assert.equal(result.skipped, false);
const saved = fs.readFileSync(path.join(publicDir, 'daily-news-0911.html'), 'utf8');
assert.doesNotMatch(saved, /吃瓜爆料平台/u);
assert.match(saved, /国产大飞机交付第100架/u);
assert.match(saved, /name="memind-content-safety"/);
assert.match(saved, /本文由 AI 辅助整理/u);
assert.equal(reviewerCalls.length, 1, 'second pass reuses cached review verdicts');
} finally {
fs.rmSync(tmpRoot, { recursive: true, force: true });
}
});
test('schedule helpers detect today page and due windows', () => {
const {
isNewsMorningPageForToday,
isLocalScheduleDue,
isWithinNewsMorningGenerateLeadWindow,
formatLocalDateParts,
} = wechatNewsMorningDraftInternals;
const now = Date.UTC(2026, 8, 10, 22, 0, 0);
const parts = formatLocalDateParts(new Date(now), 'Asia/Shanghai');
assert.equal(parts.mmdd, '0911');
assert.equal(
isNewsMorningPageForToday(
{ slug: 'daily-news-0911' },
{ date: new Date(now), timezone: 'Asia/Shanghai' },
),
true,
);
assert.equal(
isLocalScheduleDue(
{ hour: 6, minute: 0, timezone: 'Asia/Shanghai' },
now,
1,
),
true,
);
assert.equal(
isWithinNewsMorningGenerateLeadWindow(
{
pushHour: 6,
pushMinute: 0,
generateLeadMinutes: 60,
timezone: 'Asia/Shanghai',
},
Date.UTC(2026, 8, 10, 21, 15, 0),
),
true,
);
});