import assert from 'node:assert/strict'; import { mkdtemp, readFile, stat, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import test from 'node:test'; import { deriveWeixinBotIdentity, WeixinConfigStore, } from '../../../src/channels/weixin/config-store.mjs'; import { WEIXIN_RECENT_OUTBOUND_LIMIT, WEIXIN_RECENT_OUTBOUND_TTL_MS, WeixinStateStore, } from '../../../src/channels/weixin/state-store.mjs'; test('config store persists non-secret account facts atomically with restrictive permissions', async () => { const root = await mkdtemp(join(tmpdir(), 'dsh-weixin-config-')); const path = join(root, 'nested', 'config.json'); const store = await new WeixinConfigStore(path).load(); const identity = deriveWeixinBotIdentity('account@im.bot'); await store.save({ ...identity, accountId: 'account@im.bot', ownerUserId: 'owner-user', baseUrl: 'https://ilinkai.weixin.qq.com', createdAt: '2026-08-15T00:00:00.000Z', }); const raw = await readFile(path, 'utf8'); assert.match(raw, /"accountId": "account@im\.bot"/); assert.doesNotMatch(raw, /bot_token|secret-token/); assert.equal((await stat(path)).mode & 0o777, 0o600); assert.deepEqual((await new WeixinConfigStore(path).load()).list(), store.list()); }); test('config store rejects duplicate or tampered identities', async () => { const root = await mkdtemp(join(tmpdir(), 'dsh-weixin-invalid-')); const path = join(root, 'config.json'); await writeFile(path, JSON.stringify({ version: 1, accounts: [{ botId: 'wx_000000000000000000000000', accountId: 'real@im.bot', tokenRef: 'DSH_WEIXIN_BOT_TOKEN_000000000000000000000000', ownerUserId: 'owner', baseUrl: 'https://ilinkai.weixin.qq.com', }], })); await assert.rejects(new WeixinConfigStore(path).load(), /invalid account data/); }); test('state store retains sessions, deduplication, and the getUpdates cursor', async () => { const root = await mkdtemp(join(tmpdir(), 'dsh-weixin-state-')); const path = join(root, 'account', 'state.json'); const state = await new WeixinStateStore(path).load(); await state.setSession('p2p:user', 'session-1'); await state.markSeen('message-1'); await state.setGetUpdatesBuf('cursor-2'); const restored = await new WeixinStateStore(path).load(); assert.equal(restored.sessionFor('p2p:user'), 'session-1'); assert.equal(restored.hasSeen('message-1'), true); assert.equal(restored.getUpdatesBuf(), 'cursor-2'); assert.equal((await stat(path)).mode & 0o777, 0o600); }); test('state store resolves recent Weixin bot replies by exact id or one unambiguous timestamp', async () => { const root = await mkdtemp(join(tmpdir(), 'dsh-weixin-outbound-state-')); const path = join(root, 'account', 'state.json'); const state = await new WeixinStateStore(path).load(); const now = Date.now(); await state.rememberOutboundMessage({ toUserId: 'owner-user', text: '第一条机器人回复', sentAt: now - 40_000, completedAt: now - 39_900, providerMessageIds: ['client-first'], }); await state.rememberOutboundMessage({ toUserId: 'owner-user', text: '第二条机器人回复', sentAt: now - 10_000, completedAt: now - 9_900, providerMessageIds: ['client-second'], }); const restored = await new WeixinStateStore(path).load(); assert.equal(restored.recentOutboundTextFor({ toUserId: 'owner-user', messageId: 'client-first', now, }), '第一条机器人回复'); assert.equal(restored.recentOutboundTextFor({ toUserId: 'owner-user', createTimeMs: now - 10_000, now, }), '第二条机器人回复'); assert.equal(restored.recentOutboundTextFor({ toUserId: 'another-user', messageId: 'client-first', now, }), null); }); test('state store refuses to guess when a Weixin quote timestamp matches multiple bot replies', async () => { const root = await mkdtemp(join(tmpdir(), 'dsh-weixin-outbound-ambiguous-')); const state = await new WeixinStateStore(join(root, 'state.json')).load(); const now = Date.now(); await state.rememberOutboundMessage({ toUserId: 'owner-user', text: '候选一', sentAt: now - 1_000, completedAt: now - 900, }); await state.rememberOutboundMessage({ toUserId: 'owner-user', text: '候选二', sentAt: now - 800, completedAt: now - 700, }); assert.equal(state.recentOutboundTextFor({ toUserId: 'owner-user', createTimeMs: now - 850, now, }), null); }); test('state store bounds the Weixin outbound index by count and age on load', async () => { const root = await mkdtemp(join(tmpdir(), 'dsh-weixin-outbound-bounds-')); const path = join(root, 'state.json'); const now = Date.now(); const recentOutboundMessages = Array.from({ length: WEIXIN_RECENT_OUTBOUND_LIMIT + 1, }, (_, index) => ({ toUserId: 'owner-user', text: `最近回复 ${index}`, sentAt: now - 1_000, completedAt: now - 900, providerMessageIds: [`recent-${index}`], })); recentOutboundMessages.unshift({ toUserId: 'owner-user', text: '已过期回复', sentAt: now - WEIXIN_RECENT_OUTBOUND_TTL_MS - 2_000, completedAt: now - WEIXIN_RECENT_OUTBOUND_TTL_MS - 1_000, providerMessageIds: ['expired'], }); await writeFile(path, JSON.stringify({ version: 1, sessions: {}, seenMessageIds: [], getUpdatesBuf: '', recentOutboundMessages, })); const state = await new WeixinStateStore(path).load(); assert.equal(state.snapshot().recentOutboundMessages.length, WEIXIN_RECENT_OUTBOUND_LIMIT); assert.equal(state.recentOutboundTextFor({ toUserId: 'owner-user', messageId: 'expired', now, }), null); assert.equal(state.recentOutboundTextFor({ toUserId: 'owner-user', messageId: 'recent-0', now, }), null); assert.equal(state.recentOutboundTextFor({ toUserId: 'owner-user', messageId: `recent-${WEIXIN_RECENT_OUTBOUND_LIMIT}`, now, }), `最近回复 ${WEIXIN_RECENT_OUTBOUND_LIMIT}`); }); test('state store matches a recent bot reply from the timestamp encoded in msg_id', async () => { const root = await mkdtemp(join(tmpdir(), 'dsh-weixin-outbound-msg-id-')); const state = await new WeixinStateStore(join(root, 'state.json')).load(); const now = Date.now(); const providerTimestamp = now - 1_000; const messageId = ((BigInt(providerTimestamp) << 22n) | 123n).toString(); await state.rememberOutboundMessage({ toUserId: 'owner-user', text: '由真实微信消息 ID 恢复', sentAt: providerTimestamp - 7_500, completedAt: providerTimestamp - 7_400, }); assert.equal(state.recentOutboundTextFor({ toUserId: 'owner-user', messageId, now, }), '由真实微信消息 ID 恢复'); });