/** * 作息报时器(schedule_reminder)后端接口集成测试 * 独立起一个临时 express 实例(8978),只挂载 schedule_reminder 的路由, * 不依赖 8976 主服务,也不影响运行中的服务。 * * 数据库隔离策略:脚本开始时若 data/app_order_box_schedule_reminder.db 不存在, * 结束时把测试期间产生的 db / wal / shm 一并删除,保证不留测试脏数据。 * * 运行:node dev_test_scripts/integration/test_schedule_reminder_integration.js */ const fs = require('fs') const path = require('path') const express = require('express') const DB_FILE = path.join(process.cwd(), 'data', 'app_order_box_schedule_reminder.db') const DB_EXISTED_BEFORE = fs.existsSync(DB_FILE) const { bindRoutes } = require('../../src/server/app_order_box/kits/schedule_reminder') const PORT = 8978 const BASE = `http://127.0.0.1:${PORT}` let passed = 0 let failed = 0 const check = (name, ok, extra) => { if (ok) { passed += 1; console.log(` ✔ ${name}`) } else { failed += 1; console.log(` ✘ ${name}${extra ? ' → ' + extra : ''}`) } } const request = async (method, url, body) => { const res = await fetch(BASE + url, { method, headers: body === undefined ? undefined : { 'Content-Type': 'application/json' }, body: body === undefined ? undefined : JSON.stringify(body), }) const text = await res.text() let json = null try { json = JSON.parse(text) } catch (_) { /* 非 JSON */ } return { status: res.status, json, text } } const cleanupDb = () => { if (DB_EXISTED_BEFORE) return ;['', '-wal', '-shm'].forEach(suffix => { const p = DB_FILE + suffix try { if (fs.existsSync(p)) fs.unlinkSync(p) } catch (_) { /* 静默 */ } }) console.log('\n(测试库为本次新建,已清理:' + path.basename(DB_FILE) + ')') } const run = async () => { console.log('=== 作息报时器 · 后端接口集成测试 ===\n') // ---------- 1. 校时接口 ---------- console.log('[1] GET /time 校时') const timeRes = await request('GET', '/api/app_order_box/kits/schedule_reminder/time') check('HTTP 200', timeRes.status === 200, timeRes.status) check('返回 ok=true', !!timeRes.json && timeRes.json.ok === true) const epochMs = timeRes.json && timeRes.json.data ? Number(timeRes.json.data.epochMs) : NaN check('epochMs 是有效时间戳', Number.isFinite(epochMs) && Math.abs(Date.now() - epochMs) < 60 * 1000, epochMs) // ---------- 2. 日程校验 ---------- console.log('\n[2] POST /schedules 参数校验') const noTitle = await request('POST', '/api/app_order_box/kits/schedule_reminder/schedules', { items: [{ time: '07:30', name: '起床' }] }) check('缺 title → 400', noTitle.status === 400, noTitle.status) const noItem = await request('POST', '/api/app_order_box/kits/schedule_reminder/schedules', { title: '周末', weekdays: [6, 7] }) check('缺 items → 400', noItem.status === 400, noItem.status) const noWeekday = await request('POST', '/api/app_order_box/kits/schedule_reminder/schedules', { title: '周末', items: [{ time: '07:30', name: '起床' }] }) check('缺 weekdays → 400', noWeekday.status === 400, noWeekday.status) // ---------- 3. 新建日程 ---------- console.log('\n[3] POST /schedules 新建(含清洗规则)') const createRes = await request('POST', '/api/app_order_box/kits/schedule_reminder/schedules', { title: ' 周末在家 ', weekdays: [7, 6, 6, 99, 0], // 期望清洗为 [6,7] items: [ { time: '11:00', name: '做午饭', repeatTimes: 5, intervalSec: 30 }, { time: '07:30', name: '起床', repeatTimes: 99, intervalSec: 1 }, // 期望被夹到 20 / 5 { time: '25:99', name: '非法时刻' }, // 期望被过滤 ], }) check('HTTP 200', createRes.status === 200, createRes.status + ' ' + createRes.text.slice(0, 120)) const created = createRes.json && createRes.json.data check('title 已 trim', created && created.title === '周末在家', created && created.title) check('weekdays 清洗为 [6,7]', created && JSON.stringify(created.weekdays) === '[6,7]', created && JSON.stringify(created.weekdays)) check('非法时刻被过滤(剩 2 条)', created && created.items.length === 2, created && created.items.length) check('items 按时刻升序', created && created.items[0].time === '07:30' && created.items[1].time === '11:00', created && created.items.map(i => i.time).join(',')) const first = created && created.items[0] check('repeatTimes 夹到 20', first && first.repeatTimes === 20, first && first.repeatTimes) check('intervalSec 夹到 5', first && first.intervalSec === 5, first && first.intervalSec) check('默认 repeatTimes=5 / intervalSec=30', created && created.items[1].repeatTimes === 5 && created.items[1].intervalSec === 30, created && (created.items[1].repeatTimes + '/' + created.items[1].intervalSec)) const scheduleId = created && created.id // ---------- 4. 列表 ---------- console.log('\n[4] GET /schedules 列表') const listRes = await request('GET', '/api/app_order_box/kits/schedule_reminder/schedules') check('HTTP 200', listRes.status === 200, listRes.status) check('列表含新建日程', Array.isArray(listRes.json.data) && listRes.json.data.some(s => s.id === scheduleId)) // ---------- 5. 更新 ---------- console.log('\n[5] PUT /schedules 更新') const updateRes = await request('PUT', '/api/app_order_box/kits/schedule_reminder/schedules', { id: scheduleId, title: '周末在家 v2', weekdays: [6, 7], items: [{ time: '07:30', name: '起床', repeatTimes: 5, intervalSec: 30 }], }) check('HTTP 200', updateRes.status === 200, updateRes.status + ' ' + updateRes.text.slice(0, 120)) check('title 已更新', updateRes.json && updateRes.json.data.title === '周末在家 v2') check('items 已更新为 1 条', updateRes.json && updateRes.json.data.items.length === 1) const missingUpdate = await request('PUT', '/api/app_order_box/kits/schedule_reminder/schedules', { id: 'not-exist-id', title: 'x', weekdays: [1], items: [{ time: '08:00', name: 'a' }], }) check('更新不存在的日程 → 404', missingUpdate.status === 404, missingUpdate.status) // ---------- 6. 播报日志 ---------- console.log('\n[6] 播报日志 logs / logs/ack') const logRes = await request('POST', '/api/app_order_box/kits/schedule_reminder/logs', { scheduleId: scheduleId, scheduleTitle: '周末在家 v2', itemId: 'item-1', itemName: '起床', plannedAt: '2026-09-30T07:30:00+08:00', status: 'fired', }) check('写日志 HTTP 200', logRes.status === 200, logRes.status + ' ' + logRes.text.slice(0, 120)) const logId = logRes.json && logRes.json.data && logRes.json.data.id check('返回日志 id', Number.isFinite(Number(logId)) && Number(logId) > 0, logId) check('状态为 fired', logRes.json && logRes.json.data.status === 'fired') const missingLog = await request('POST', '/api/app_order_box/kits/schedule_reminder/logs', { itemName: 'x' }) check('缺 itemId → 400', missingLog.status === 400, missingLog.status) const ackRes = await request('POST', '/api/app_order_box/kits/schedule_reminder/logs/ack', { id: logId }) check('确认 HTTP 200', ackRes.status === 200, ackRes.status) check('状态变为 acked', ackRes.json && ackRes.json.data && ackRes.json.data.status === 'acked', ackRes.json && ackRes.json.data && ackRes.json.data.status) const logsRes = await request('GET', '/api/app_order_box/kits/schedule_reminder/logs') check('日志列表 HTTP 200', logsRes.status === 200, logsRes.status) check('列表含刚写入的记录', Array.isArray(logsRes.json.data) && logsRes.json.data.some(l => Number(l.id) === Number(logId))) // ---------- 7. 语音预合成参数校验 ---------- console.log('\n[7] POST /tts/prepare 参数校验(不做真实合成)') const ttsBad = await request('POST', '/api/app_order_box/kits/schedule_reminder/tts/prepare', { schedule: {} }) check('空 schedule → 400', ttsBad.status === 400, ttsBad.status) // ---------- 8. 删除 ---------- console.log('\n[8] DELETE /schedules 删除') const delRes = await request('DELETE', `/api/app_order_box/kits/schedule_reminder/schedules?id=${encodeURIComponent(scheduleId)}`) check('HTTP 200', delRes.status === 200, delRes.status) const afterDel = await request('GET', '/api/app_order_box/kits/schedule_reminder/schedules') check('删除后列表不再包含', Array.isArray(afterDel.json.data) && !afterDel.json.data.some(s => s.id === scheduleId)) console.log(`\n=== 结果:通过 ${passed} 项,失败 ${failed} 项 ===`) return failed === 0 } const app = express() app.use(express.json()) bindRoutes(app) const server = app.listen(PORT, async () => { let ok = false try { ok = await run() } catch (e) { console.log('\n✘ 测试执行异常:' + String((e && e.stack) || e)) failed += 1 } server.close(() => { cleanupDb() process.exit(ok ? 0 : 1) }) })