Files
Toolbox/dev_test_scripts/unit/test_calendar_batch_tool.js
yangxiangyuan db7648eb4d feat(mp, web, calendar): 新增微信收藏夹与日历批量生成功能
- 新增微信小程序版个人收藏夹全套功能,包含聚合首页、收藏列表、编辑、详情页面与配套后端API
- 新增web版时光助手门户与个人收藏夹工具,支持与小程序共享后端数据
- 优化日历提醒工具:替换本地bootstrap资源为公共CDN版本,新增批量生成循环提醒的前端界面与后端逻辑,新增数据库batch_id字段,更新鉴权配置文件
- 补充项目配置、站点地图、调试诊断脚本等配套文件
- 完善各模块的页面配置与样式文件
2026-09-02 22:20:37 +08:00

174 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const path = require('path');
process.chdir(path.resolve(__dirname, '..', '..'));
const fs = require('fs');
const testDir = path.join(__dirname, '..', 'debug', 'calendar_batch_test');
if (!fs.existsSync(testDir)) fs.mkdirSync(testDir, { recursive: true });
const ts = Date.now();
const testDB = path.join(testDir, `cal_test_${ts}.db`);
const testWal = testDB + '-wal';
const testShm = testDB + '-shm';
const srcDB = path.join(process.cwd(), 'data', 'calendar_reminder_backups', 'calendar_reminder_20260831_122130.db');
const srcWal = srcDB + '-wal';
const srcShm = srcDB + '-shm';
console.log('Copying source snapshot DB to:', testDB);
fs.copyFileSync(srcDB, testDB);
if (fs.existsSync(srcWal)) fs.copyFileSync(srcWal, testWal);
if (fs.existsSync(srcShm)) fs.copyFileSync(srcShm, testShm);
// 把 calendar_reminder.js 的 DB_PATH 指向我们的临时 DB 副本
// 做法:在 require calendar_reminder 前 hook better-sqlite3? 或者更简单:重写 require cache
// 更简单:用进程内 NODE_OPTIONS 不行,但 require.cache 可以改。
// 直接 patch module exports 之前先在 calendar_reminder 里用全局变量?
// 最快做法:写一个 wrapper,覆盖 `const DB_PATH` 那行 -> 重新写 tempDBPath 赋值到 module scope 不方便,改用:
// 把 `data/calendar_reminder.db` 临时 symlink/copy?但 Windows symlink 麻烦。
// 就用最简单:创建独立 require 上下文。直接把 calendar_reminder.js 的 src 读出来、替换 DB_PATH,然后 vm.runInNewContext 太麻烦。
// 更简便:我们用 require 的 module.__proto 拦截。
// 改文件替换(完成后改回):
const modFile = path.join(process.cwd(), 'src', 'server', 'calendar_reminder.js');
const orig = fs.readFileSync(modFile, 'utf8');
const modded = orig.replace(
/const DB_PATH = path\.join\(process\.cwd\(\), 'data', 'calendar_reminder\.db'\)/,
`const DB_PATH = process.env.CALENDAR_DB_OVERRIDE || path.join(process.cwd(), 'data', 'calendar_reminder.db')`
);
if (modded === orig) { console.error('ERR: replace 未生效,DB_PATH 模式变了'); process.exit(3); }
const backup = path.join(testDir, `calendar_reminder_${ts}.bak.js`);
fs.copyFileSync(modFile, backup);
fs.writeFileSync(modFile, modded, 'utf8');
console.log('patched calendar_reminder.js to env override (backup at', backup, ')');
process.env.CALENDAR_DB_OVERRIDE = testDB;
console.log('CALENDAR_DB_OVERRIDE =', process.env.CALENDAR_DB_OVERRIDE);
// 清 require cache
for (const k of Object.keys(require.cache)) {
if (k.includes('calendar_reminder') || k.includes('calendar_voice')) delete require.cache[k];
}
const express = require('express');
const Database = require('better-sqlite3');
const mod = require(modFile);
const db = new Database(testDB);
console.log('[自测] 确保 migrate 已执行:先 bindRoutes 一次');
const express_check_app = express();
mod.bindRoutes(express_check_app);
const cols = new Set(db.prepare("PRAGMA table_info(events)").all().map(r => r.name));
console.log('[自测] events 列(迁移后):', [...cols].join(','));
if (!cols.has('batch_id')) { console.error('ERR: 迁移后仍然缺 batch_id'); process.exit(5) }
const batchesExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='calendar_batches'").get();
if (!batchesExists) { console.error('ERR: calendar_batches 未创建'); process.exit(6) }
const port = 8791;
const app = express();
app.use(express.json({ limit: '10mb' }));
app.use((req, res, next) => { req.headers.cookie = 'calendar_reminder_gate=1'; next(); });
mod.bindRoutes(app);
const params = {
batchName: '【自测】洗被子提醒 2026Q4-2027Q2',
mode: 'monthlyByDay',
rangeStart: '2026-09-01',
rangeEnd: '2027-06-30',
monthlyEvery: 2,
monthlyDays: '1-5',
startTime: '09:00',
endTime: '10:00',
remindTime: '08:30',
allDay: false,
completed: false,
title: '洗被子提醒({MM}-{dd})',
content: ''
};
const server = app.listen(port, async () => {
let exitCode = 0;
try {
const axios = require('axios');
const base = `http://127.0.0.1:${port}`;
const headers = { 'Content-Type': 'application/json', 'Cookie': 'calendar_reminder_gate=1' };
const ax = axios.create({ headers, validateStatus: () => true });
const r1 = await ax.post(`${base}/api/calendar_reminder/batch/preview`, params);
console.log('[自测] 1.preview status=', r1.status, 'generated=', r1.data.generated, 'summary=', r1.data.summary);
if (r1.status !== 200 || !r1.data.ok) { console.error('preview fail', r1.status, r1.data); throw new Error('preview') }
if (r1.data.generated !== 25) { console.error('expected 25, got', r1.data.generated); throw new Error('count') }
const batchId = r1.data.batchId;
console.log('[自测] batch_id =', batchId);
const r2 = await ax.post(`${base}/api/calendar_reminder/batch/apply`, { batch_id: batchId, params: r1.data.params_saved, sql_full: r1.data.sql_full, skip_external_sync: true });
console.log('[自测] 2.apply=', r2.status, r2.data);
if (r2.status !== 200 || !r2.data.ok) throw new Error('apply');
const counts = db.prepare("SELECT deleted, COUNT(1) c FROM events WHERE batch_id=? GROUP BY deleted").all(batchId);
console.log('[自测] 3.active counts=', counts);
const active = (counts.find(r => r.deleted === 0) || {}).c || 0;
if (active !== 25) throw new Error('active !=25, got ' + active);
const batchRow = db.prepare("SELECT id, status, inserted_count, dedup_count, rollback_count, pre_existing_count, skip_external_sync FROM calendar_batches WHERE id=?").get(batchId);
console.log('[自测] 4.batch row=', batchRow);
const rL = await ax.get(`${base}/api/calendar_reminder/batches?q=%E6%B4%97%E8%A2%AB%E5%AD%90`);
console.log('[自测] 5.batches list len=', rL.data.data && rL.data.data.length);
const rD = await ax.get(`${base}/api/calendar_reminder/batch/detail?id=${encodeURIComponent(batchId)}`);
console.log('[自测] 6.detail counts=', rD.data.batch && rD.data.batch.counts, 'diag ok?', !!rD.data.diagnostics && !!rD.data.diagnostics.by_batch_id);
const rE = await ax.get(`${base}/api/calendar_reminder/batch/export_sql?id=${encodeURIComponent(batchId)}`);
console.log('[自测] 7.export_sql len=', typeof rE.data === 'string' ? rE.data.length : (Buffer.isBuffer(rE.data) ? rE.data.length : '??'), 'headers?', rE.headers && (rE.headers['content-disposition'] || rE.headers['content-type']));
const rR = await ax.post(`${base}/api/calendar_reminder/batch/rollback`, { id: batchId, hard: false });
console.log('[自测] 8.rollback=', rR.status, rR.data);
const c2 = db.prepare("SELECT deleted, COUNT(1) c FROM events WHERE batch_id=? GROUP BY deleted").all(batchId);
console.log('[自测] 9.post-rollback counts=', c2);
const d = (c2.find(r => r.deleted === 1) || {}).c || 0;
if (d !== 25) throw new Error('deleted!=25, got ' + d);
const rRepeat = await ax.post(`${base}/api/calendar_reminder/batch/preview`, Object.assign({}, params, { batchName: '【自测-复制】洗被子' }));
console.log('[自测] 10.复制后的新批次 preview 预重(deleted=1 不命中去重)=', rRepeat.data.pre_existing_count);
if (rRepeat.data.pre_existing_count !== 0) throw new Error('pre_existing_count !=0');
// 再做 apply 回滚然后 rollback 再做第二次去重测试
const bid2 = rRepeat.data.batchId;
const rA2 = await ax.post(`${base}/api/calendar_reminder/batch/apply`, { batch_id: bid2, params: rRepeat.data.params_saved, sql_full: rRepeat.data.sql_full, skip_external_sync: true });
console.log('[自测] 11.复制 apply=', rA2.status, rA2.data);
const rDup = await ax.post(`${base}/api/calendar_reminder/batch/preview`, Object.assign({}, params));
console.log('[自测] 12.相同参数再预览(因为 bid2 刚 active=25,所以 pre_existing_count=25)=', rDup.data.pre_existing_count);
if (rDup.data.pre_existing_count !== 25) throw new Error('pre_existing_count should be 25');
// 清理两条批次元数据 + events,让临时 DB 恢复干净(虽然是临时的,但也清一下)
const del1 = db.prepare("DELETE FROM events WHERE batch_id IN (?,?)").run(batchId, bid2).changes;
const del2 = db.prepare("DELETE FROM calendar_batches WHERE id IN (?,?)").run(batchId, bid2).changes;
console.log('[自测] 13.cleanup del events=', del1, ', del batches=', del2);
// 再测 monthlyByNthWeekday:2026-09 到 2027-01 每月第 2 个周一,应得到 5 条
const wd = { batchName: '房租 demo', mode: 'monthlyByNthWeekday', rangeStart: '2026-09-01', rangeEnd: '2027-01-31', monthlyEvery2: 1, nthWeek: 2, nthWeekday: 1, title: '交房租提醒 {date}', content: '', startTime: '09:00' };
const rw = await ax.post(`${base}/api/calendar_reminder/batch/preview`, wd);
console.log('[自测] 14.monthlyByNthWeekday=第2个周一 9月-1月 生成=', rw.data.generated, (rw.data.sample_list||[]).map(x=>x.start));
if (rw.data.generated !== 5) throw new Error('monthlyByNthWeekday expected 5, got ' + rw.data.generated);
// weekly:每 2 周的周一、周三,2026-09-01~2026-10-15(每 2 周 2 条,6 周 * ?大概 6~7 条)
const wk = { batchName: '健身', mode: 'weekly', rangeStart: '2026-09-01', rangeEnd: '2026-10-15', weeklyEvery: 2, weekdays: [1,3], title: '健身 {date} 周{W}', content: '', startTime: '20:00', endTime: '22:00', remindTime: '19:30' };
const rwk = await ax.post(`${base}/api/calendar_reminder/batch/preview`, wk);
console.log('[自测] 15.weekly 每 2 周 Mon/Wed 生成=', rwk.data.generated, 'list=', (rwk.data.sample_list||[]).map(x=>x.start + ' ' + x.title));
// daily:每 3 天 2026-09-01~2026-09-20 => 7条 (1 4 7 10 13 16 19)
const dl = { batchName: '喝水', mode: 'daily', rangeStart: '2026-09-01', rangeEnd: '2026-09-20', dailyEvery: 3, title: '喝水打卡 {date}', startTime: '10:00' };
const rdl = await ax.post(`${base}/api/calendar_reminder/batch/preview`, dl);
console.log('[自测] 16.daily 每3天 9/1-20 生成=', rdl.data.generated, 'list=', (rdl.data.sample_list||[]).map(x=>x.start));
if (rdl.data.generated !== 7) throw new Error('daily expected 7, got ' + rdl.data.generated);
console.log('\n✅ 自测全部通过。退出。');
} catch (e) {
console.error('[自测] 异常:', e.message, e.stack);
exitCode = 1;
} finally {
// 恢复 calendar_reminder.js 源码
fs.copyFileSync(backup, modFile);
fs.unlinkSync(backup);
console.log('restored calendar_reminder.js 原文');
// 删临时 DB
try { fs.unlinkSync(testDB) } catch {}
try { fs.unlinkSync(testWal) } catch {}
try { fs.unlinkSync(testShm) } catch {}
server.close(() => process.exit(exitCode));
setTimeout(() => process.exit(exitCode), 2000).unref();
}
});