补充发送侧分组与错误分支的测试覆盖,保留43101原始错误信息的修复

- reminder.js:43101 跳过日志改为记录微信原始错误信息,便于和「额度耗尽」的
  兜底文案区分排查(前一实施者已验证,此次原样带入提交)
- reminder.test.js:新增 runOnce 按 openid 分组结算用例,验证多用户额度互不
  串号;将「非额度类错误」用例扩到 2 条数据,证明该分支不会误中止后续发送
- 新建 wx.test.js:验证 sendSubscribeMessage 在微信返回非 0 errcode 时,
  抛出的 Error 对象确实带有正确的 errcode 属性

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
yuming
2026-08-16 09:07:46 +08:00
parent 6548577a98
commit 57470ade4f
3 changed files with 91 additions and 7 deletions
+50 -3
View File
@@ -103,21 +103,29 @@ test('遇到 43101 立即归零并中止本用户剩余发送', async () => {
assert.strictEqual(r.skipped, 3, '含失败那条在内全部计为 skipped')
})
test('非额度类错误只记 failed,不动余额', async () => {
test('非额度类错误只记 failed,不中止后续发送,也不动余额', async () => {
resetLogs()
quota.grant('uE', 4)
let called = 0
const restore = wx.sendSubscribeMessage
wx.sendSubscribeMessage = async () => {
called++
const e = new Error('网络炸了')
e.errcode = 40003
throw e
}
const items = [makeAnniv('E1', 'uE', '甲', 'high', 0, 3)]
// 至少 2 条数据:如果有人误把 halted = true 加进非额度错误分支,
// 第二条就不会被尝试,called 会停在 1,这条断言就能抓到
const items = [
makeAnniv('E1', 'uE', '甲', 'high', 0, 3),
makeAnniv('E2', 'uE', '乙', 'high', 0, 3)
]
const r = await reminder.runForUser('uE', items, TODAY)
wx.sendSubscribeMessage = restore
assert.strictEqual(r.fail, 1)
assert.strictEqual(called, 2, '第二条也应被尝试,证明非额度错误不会误中止后续发送')
assert.strictEqual(r.fail, 2)
assert.strictEqual(quota.getBalance('uE'), 4, '配置/网络错误不该动余额')
})
@@ -129,3 +137,42 @@ test('skipped 会写入 remind_logs 便于排查', async () => {
assert.ok(row, '应写入 skipped 日志')
assert.strictEqual(row.error, 'quota_exhausted')
})
// runOnce 是从数据库读数据的(SELECT * FROM anniversaries WHERE remindEnabled = 1),
// 不能像 runForUser 那样直接传数组,必须真的往临时库插行
function insertAnniv(id, openid, personName, offsetDays, remindDays) {
const d = inDays(offsetDays)
db.prepare(`
INSERT INTO anniversaries
(id, openid, personId, personName, type, isLunar, solarYear, solarMonth, solarDay,
importance, remindEnabled, remindDays, createTime, updateTime)
VALUES (?, ?, 'p', ?, 'birthday', 0, ?, ?, ?, 'high', 1, ?, 0, 0)
`).run(id, openid, personName, d.getFullYear(), d.getMonth() + 1, d.getDate(), remindDays)
}
test('runOnce 按 openid 分组结算,各用户额度互不影响', async () => {
resetLogs()
// 会读到库里所有 remindEnabled=1 的行,先清空避免受其它用例残留数据干扰
db.prepare('DELETE FROM anniversaries').run()
quota.grant('uG', 5) // uG 有额度
// uH 不 grant,余额保持 0
insertAnniv('G1', 'uG', '甲', 0, 3) // 今天到期
insertAnniv('H1', 'uH', '乙', 0, 3) // 今天到期
let called = 0
const restore = wx.sendSubscribeMessage
wx.sendSubscribeMessage = async () => { called++; return { errcode: 0 } }
const r = await reminder.runOnce()
wx.sendSubscribeMessage = restore
assert.strictEqual(r.total, 2)
assert.strictEqual(r.ok, 1, '有额度的 uG 应正常发出')
assert.strictEqual(r.skipped, 1, '没额度的 uH 应被跳过')
assert.strictEqual(r.fail, 0)
assert.strictEqual(called, 1, '没额度的用户不该真的发起请求,两用户不能互相借额度')
assert.strictEqual(quota.getBalance('uG'), 4, 'uG 消费后余额正确减少')
assert.strictEqual(quota.getBalance('uH'), 0, 'uH 的余额不受 uG 影响')
})
+35
View File
@@ -0,0 +1,35 @@
const test = require('node:test')
const assert = require('node:assert')
const { useTempDb } = require('./helper')
// wx.js 本身不连库,但按项目约定统一在 require 业务模块前处理好临时库,
// 避免以后 wx.js 引入了连库逻辑时这份测试悄悄读写到真实数据库。
useTempDb()
const axios = require('axios')
const wx = require('../src/wx')
test('微信返回非 0 errcode 时,抛出的 Error 对象应带上原始 errcode', async () => {
const restoreGet = axios.get
const restorePost = axios.post
// getAccessToken 走 axios.getsendSubscribeMessage 走 axios.post,两个都要 mock
axios.get = async () => ({ data: { access_token: 'fake_token', expires_in: 7200 } })
axios.post = async () => ({ data: { errcode: 43101, errmsg: 'user refuse to accept the msg' } })
await assert.rejects(
() => wx.sendSubscribeMessage({
touser: 'oTest',
page: 'pages/index/index',
templateId: 'tplTest',
data: {}
}),
(err) => {
assert.strictEqual(err.errcode, 43101, 'Error 对象应带上微信返回的原始 errcode,而不是靠字符串匹配')
return true
}
)
axios.get = restoreGet
axios.post = restorePost
})