const test = require('node:test') const assert = require('node:assert/strict') const fs = require('node:fs') const path = require('node:path') const vm = require('node:vm') const ts = require('typescript') function load(file, deps = {}) { const exports = {} vm.runInNewContext(ts.transpileModule(fs.readFileSync(path.join(__dirname, '..', file), 'utf8'), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2019 } }).outputText, { exports, require: id => deps[id], setTimeout, clearTimeout }) return exports } const ritual = load('src/utils/incenseRitual.ts') const mock = load('src/utils/tiangongMock.ts', { './incenseRitual': ritual }) const { createTiangongStore } = load('src/utils/tiangongStore.ts', { './tiangongMock': mock, './incenseRitual': ritual }) const defer = () => { let resolve, reject; const promise = new Promise((ok, fail) => { resolve = ok; reject = fail }); return { promise, resolve, reject } } function harness() { let key = 'account-a', time = 1000, rows = [{ id: 81, name: '财神', holdingId: 726, active: true, incenseCount: 7, days: 3 }] let loader = async () => rows, offer = async () => ({ id: 99 }), lifecycle = async () => ({ id: 101 }), dedicate = async () => ({ id: 100 }) const calls = [] const store = createTiangongStore({ key: () => key, now: () => time, resolve: name => name === '财神' ? 'wealth' : undefined, load: () => loader(), offer: (...args) => { calls.push(args); return offer(...args) }, lifecycle: (...args) => { calls.push(args); return lifecycle(...args) }, dedicate: (...args) => { calls.push(args); return dedicate(...args) } }) return { store, calls, key: value => { key = value }, time: value => { time = value }, rows: value => { rows = value }, load: value => { loader = value }, offer: value => { offer = value }, lifecycle: value => { lifecycle = value }, dedicate: value => { dedicate = value } } } test('真实状态不预置财神,按后端持有ID提交,后端确认后才计数', async () => { const h = harness() assert.equal(h.store.snapshot().deities.wealth.unlocked, false) await h.store.refresh() const response = defer(); h.offer(() => response.promise) const request = h.store.incense('wealth') assert.equal(h.store.snapshot().deities.wealth.incenseCount, 7) assert.equal(h.store.snapshot().deities.wealth.lit, false) assert.equal(h.calls[0][0], 726); assert.equal(h.calls[0][1], 1) response.resolve({ id: 10 }); await request assert.equal(h.store.snapshot().deities.wealth.incenseCount, 8) assert.equal(h.store.snapshot().deities.wealth.incenseUntil, 16000) }) test('列表图、详情图、介绍独立保存,刷新及切换账号不串用旧素材', async () => { const h = harness() h.rows([{ id: 81, name: '财神', holdingId: 726, active: true, incenseCount: 7, days: 3, iconUrl: '/file/list.png', iconDetailUrl: '/file/detail.png', description: '接口介绍\n第二行' }]) await h.store.refresh() const first = h.store.snapshot().deities.wealth assert.equal(first.iconUrl, '/file/list.png'); assert.equal(first.iconDetailUrl, '/file/detail.png') assert.equal(first.description, '接口介绍\n第二行') h.rows([{ id: 81, name: '财神', active: false, incenseCount: 0, days: 0, iconUrl: '/file/new-list.png', iconDetailUrl: '/file/new-detail.png', description: '更新的介绍' }]) await h.store.refresh(true) const second = h.store.snapshot().deities.wealth assert.equal(second.unlocked, false); assert.equal(second.iconDetailUrl, '/file/new-detail.png') assert.equal(second.description, '更新的介绍') h.key('another-account') assert.equal(h.store.snapshot().deities.wealth.iconDetailUrl, undefined) }) test('失败无计数无自动重试;晚到查询不覆盖已确认上香', async () => { const h = harness(); await h.store.refresh() h.offer(async () => { throw new Error('网络失败') }) await assert.rejects(h.store.incense('wealth'), /网络失败/) assert.equal(h.calls.length, 1); assert.equal(h.store.snapshot().deities.wealth.incenseCount, 7) const read = defer(); h.load(() => read.promise) const refreshing = h.store.refresh(true) h.offer(async () => ({ id: 1 })); await h.store.incense('wealth') read.resolve([{ name: '财神', holdingId: 726, active: true, incenseCount: 7 }]); await refreshing assert.equal(h.store.snapshot().deities.wealth.incenseCount, 8) }) test('乱序并发响应逐次累计,不让旧点击缩短最后截止时间', async () => { const h = harness(); await h.store.refresh() const a = defer(), b = defer(); let count = 0 h.offer(() => (++count === 1 ? a : b).promise) const first = h.store.incense('wealth'); h.time(4000); const second = h.store.incense('wealth') b.resolve({ id: 2 }); assert.equal((await second).startsRound, true) a.resolve({ id: 1 }); assert.equal((await first).startsRound, false) assert.equal(h.store.snapshot().deities.wealth.incenseCount, 9) assert.equal(h.store.snapshot().deities.wealth.incenseUntil, 19000) h.time(19001); assert.equal(h.store.snapshot().deities.wealth.lit, false) }) test('写入在途不采纳可能已包含该流水的GET,避免回包后再次加一', async () => { const h = harness(); await h.store.refresh() const response = defer(), read = defer() h.load(() => read.promise) const refreshing = h.store.refresh(true) h.offer(() => response.promise); const sending = h.store.incense('wealth') read.resolve([{ name: '财神', holdingId: 726, active: true, incenseCount: 8 }]); await refreshing assert.equal(h.store.snapshot().deities.wealth.incenseCount, 7) await h.store.refresh(true) response.resolve({ id: 2 }); await sending assert.equal(h.store.snapshot().deities.wealth.incenseCount, 8) }) test('切换账号丢弃旧响应,不以Mock兜底;未绑定不可提交', async () => { const h = harness(); await h.store.refresh() const response = defer(); h.offer(() => response.promise) const sending = h.store.incense('wealth') h.key('account-b'); assert.equal(h.store.snapshot().deities.wealth.unlocked, false) response.resolve({ id: 10 }); await assert.rejects(sending, /登录状态/) h.rows([]); await h.store.refresh() await assert.rejects(h.store.incense('wealth'), /绑定/) h.load(async () => { throw new Error('未部署') }); await assert.rejects(h.store.refresh(true), /未部署/) assert.equal(h.store.snapshot().deities.wealth.unlocked, false) }) test('祈愿和任意姓名回向去空白提交,不携带功德或注册收件人ID', async () => { const h = harness(); await h.store.refresh() await h.store.wish('wealth', ' 愿顺遂 ') await h.store.dedicate('wealth', ' 家人 ', ' 平安 ') assert.deepEqual(h.calls, [[726, 2, '愿顺遂'], [726, '家人', '平安']]) assert.equal(h.store.snapshot().deities.wealth.latestPrayer.prayerContent, '愿顺遂') assert.equal(h.store.snapshot().deities.wealth.latestDedication.dedicationTarget, '家人') assert.equal(h.store.snapshot().deities.wealth.latestDedication.dedicationContent, '平安') await assert.rejects(h.store.wish('wealth', ' '), /愿望/) await assert.rejects(h.store.dedicate('wealth', '', '祝愿'), /回向对象/) assert.equal(h.calls.length, 2) }) test('最新祈愿回向随查询加载,失效或账号切换清空,不影响其他神仙', async () => { const h = harness() const row = { id: 81, name: '财神', holdingId: 726, active: true, incenseCount: 0, days: 1, latestPrayer: { offeringId: 10, prayerContent: '已保存愿望' }, latestDedication: { offeringId: 11, dedicationTarget: '众生', dedicationContent: null } } h.rows([row]); await h.store.refresh() const first = h.store.snapshot().deities assert.equal(first.wealth.latestPrayer.prayerContent, '已保存愿望') assert.equal(first.wealth.latestDedication.dedicationTarget, '众生') assert.equal(first.wealth.latestDedication.dedicationContent, null) assert.equal(first.wenchang.latestPrayer, undefined) first.wealth.latestPrayer.prayerContent = '修改快照不污染缓存' assert.equal(h.store.snapshot().deities.wealth.latestPrayer.prayerContent, '已保存愿望') h.rows([{ ...row, latestPrayer: null, latestDedication: null }]); await h.store.refresh(true) assert.equal(h.store.snapshot().deities.wealth.latestPrayer, null) assert.equal(h.store.snapshot().deities.wealth.latestDedication, null) h.rows([row]); await h.store.refresh(true) h.key('account-b') assert.equal(h.store.snapshot().deities.wealth.latestPrayer, undefined) assert.equal(h.store.snapshot().deities.wealth.latestDedication, undefined) }) test('祈愿确认后才更新回显,失败和账号切换不写入旧账号内容', async () => { const h = harness(); await h.store.refresh() const response = defer(); h.offer(() => response.promise) const request = h.store.wish('wealth', '待确认的愿望') assert.equal(h.store.snapshot().deities.wealth.latestPrayer, null) response.resolve({ id: 105 }); await request assert.equal(h.store.snapshot().deities.wealth.latestPrayer.prayerContent, '待确认的愿望') h.offer(async () => { throw new Error('提交失败') }) await assert.rejects(h.store.wish('wealth', '失败愿望'), /提交失败/) assert.equal(h.store.snapshot().deities.wealth.latestPrayer.prayerContent, '待确认的愿望') const pending = defer(); h.offer(() => pending.promise) const sending = h.store.wish('wealth', '旧账号愿望') h.key('account-b'); h.store.snapshot() pending.resolve({ id: 106 }); await assert.rejects(sending, /登录状态/) assert.equal(h.store.snapshot().deities.wealth.latestPrayer, undefined) }) test('回向成功后可回显空祝愿,失败不覆盖已保存对象', async () => { const h = harness(); await h.store.refresh() await h.store.dedicate('wealth', '家人', '平安') h.dedicate(async () => { throw new Error('提交失败') }) await assert.rejects(h.store.dedicate('wealth', '失败对象', ''), /提交失败/) assert.equal(h.store.snapshot().deities.wealth.latestDedication.dedicationTarget, '家人') const response = defer(); h.dedicate(() => response.promise) const sending = h.store.dedicate('wealth', '众生', '') assert.equal(h.store.snapshot().deities.wealth.latestDedication.dedicationContent, '平安') response.resolve({ id: 101 }); await sending assert.equal(h.store.snapshot().deities.wealth.latestDedication.dedicationTarget, '众生') assert.equal(h.store.snapshot().deities.wealth.latestDedication.dedicationContent, '') }) test('送神和重新请神使用独立流水,成功后才改变状态且保留次数', async () => { const h = harness(); await h.store.refresh() const response = defer(); h.lifecycle(() => response.promise) const sending = h.store.sendOff('wealth') assert.equal(h.store.snapshot().deities.wealth.unlocked, true) await assert.rejects(h.store.sendOff('wealth'), /提交/) await assert.rejects(h.store.incense('wealth'), /提交/) response.resolve({ id: 102 }); await sending const stopped = h.store.snapshot().deities.wealth assert.equal(stopped.unlocked, false); assert.equal(stopped.canInvite, true) assert.equal(stopped.incenseCount, 7); assert.equal(stopped.incenseUntil, 0) assert.equal(mock.hasBoundDeity(h.store.snapshot()), true) await assert.rejects(h.store.wish('wealth', '愿顺遂'), /绑定/) h.lifecycle(async () => ({ id: 103 })) await h.store.invite('wealth') assert.equal(h.store.snapshot().deities.wealth.unlocked, true) assert.equal(h.store.snapshot().deities.wealth.incenseCount, 7) assert.deepEqual(h.calls, [[726, 2], [726, 1]]) }) test('刷新后只允许已送神持有关系重新请神,失败及账号切换不伪造成功', async () => { const h = harness() h.rows([{ id: 81, name: '财神', holdingId: 726, holdingStatus: 0, active: false, incenseCount: 7 }]) await h.store.refresh(); assert.equal(h.store.snapshot().deities.wealth.canInvite, true) h.lifecycle(async () => { throw new Error('设备已解绑') }) await assert.rejects(h.store.invite('wealth'), /设备已解绑/) assert.equal(h.store.snapshot().deities.wealth.unlocked, false) const response = defer(); h.lifecycle(() => response.promise) const sending = h.store.invite('wealth'); h.key('account-b'); h.store.snapshot() response.resolve({ id: 12 }); await assert.rejects(sending, /登录状态/) assert.equal(h.store.snapshot().deities.wealth.unlocked, false) for (const status of [undefined, 1, 3]) { h.rows([{ id: 81, name: '财神', holdingId: 726, holdingStatus: status, active: false }]) await h.store.refresh(true) await assert.rejects(h.store.invite('wealth'), /当前状态/) } }) test('回向对象允许255字,祝愿可空;上香在途不能送神', async () => { const h = harness(); await h.store.refresh() await h.store.dedicate('wealth', '甲'.repeat(255), '') await assert.rejects(h.store.dedicate('wealth', '甲'.repeat(256), ''), /回向对象/) await assert.rejects(h.store.dedicate('wealth', '甲', '愿'.repeat(501)), /500/) const response = defer(); h.offer(() => response.promise) const sending = h.store.incense('wealth') await assert.rejects(h.store.sendOff('wealth'), /提交/) response.resolve({ id: 23 }); await sending assert.equal(h.store.snapshot().deities.wealth.incenseCount, 8) }) test('送神失败保留供奉状态,无效流水不显示成功,晚到列表不撤销已送神', async () => { const h = harness(); await h.store.refresh() h.lifecycle(async () => { throw new Error('网络失败') }) await assert.rejects(h.store.sendOff('wealth'), /网络失败/) assert.equal(h.store.snapshot().deities.wealth.unlocked, true) h.lifecycle(async () => ({ id: 0 })) await assert.rejects(h.store.sendOff('wealth'), /有效记录/) assert.equal(h.store.snapshot().deities.wealth.unlocked, true) const read = defer(); h.load(() => read.promise) const refreshing = h.store.refresh(true) h.lifecycle(async () => ({ id: 15 })); await h.store.sendOff('wealth') read.resolve([{ id: 81, name: '财神', holdingId: 726, active: true, incenseCount: 7 }]); await refreshing assert.equal(h.store.snapshot().deities.wealth.unlocked, false) assert.equal(h.store.snapshot().deities.wealth.canInvite, true) })