import assert from 'node:assert/strict'; import test from 'node:test'; import { buildWechatVoiceRecoVoiceId, queryWechatVoiceRecoResult, transcribeWechatVoiceViaRecoApi, uploadWechatVoiceForReco, } from './wechat-voice-reco.mjs'; test('buildWechatVoiceRecoVoiceId prefers msgId', () => { assert.equal( buildWechatVoiceRecoVoiceId({ msgId: '7670473258902224896', mediaId: 'media-1' }), '7670473258902224896', ); }); test('transcribeWechatVoiceViaRecoApi uploads mp3 and polls reco result', async () => { const calls = []; let queryCount = 0; const mp3Buffer = Buffer.from('fake-mp3'); const text = await transcribeWechatVoiceViaRecoApi({ accessToken: 'token-1', voiceBuffer: Buffer.from('fake-amr'), format: 'amr', voiceId: 'voice-1', wechatFetch: async (url, init = {}) => { calls.push([String(url), init.method ?? 'GET']); if (String(url).includes('/addvoicetorecofortext')) { assert.equal(init.method, 'POST'); return new Response(JSON.stringify({ errcode: 0, errmsg: 'ok' }), { status: 200, headers: { 'Content-Type': 'application/json' }, }); } if (String(url).includes('/queryrecoresultfortext')) { queryCount += 1; const result = queryCount >= 2 ? '帮我看看最近的天气' : ''; return new Response(JSON.stringify({ errcode: 0, errmsg: 'ok', result }), { status: 200, headers: { 'Content-Type': 'application/json' }, }); } throw new Error(`unexpected url: ${url}`); }, convertToMp3: async () => mp3Buffer, pollIntervalMs: 1, pollTimeoutMs: 50, now: () => Date.now(), }); assert.equal(text, '帮我看看最近的天气'); assert.equal(calls.some(([url]) => url.includes('/addvoicetorecofortext')), true); assert.ok(queryCount >= 2); }); test('transcribeWechatVoiceViaRecoApi returns empty when mp3 conversion fails', async () => { let fetchCalled = false; const text = await transcribeWechatVoiceViaRecoApi({ accessToken: 'token-1', voiceBuffer: Buffer.from('fake-amr'), voiceId: 'voice-1', wechatFetch: async () => { fetchCalled = true; return new Response('{}', { status: 200 }); }, convertToMp3: async () => null, }); assert.equal(text, ''); assert.equal(fetchCalled, false); }); test('uploadWechatVoiceForReco throws on WeChat business error', async () => { await assert.rejects( () => uploadWechatVoiceForReco({ accessToken: 'token-1', voiceId: 'voice-1', mp3Buffer: Buffer.from('fake-mp3'), wechatFetch: async () => new Response(JSON.stringify({ errcode: 40010, errmsg: 'invalid voice size' }), { status: 200, headers: { 'Content-Type': 'application/json' }, }), }), /invalid voice size/, ); }); test('queryWechatVoiceRecoResult retries not-ready as empty result during polling', async () => { let queryCount = 0; const text = await transcribeWechatVoiceViaRecoApi({ accessToken: 'token-1', voiceBuffer: Buffer.from('fake-amr'), voiceId: 'voice-1', wechatFetch: async (url) => { if (String(url).includes('/addvoicetorecofortext')) { return new Response(JSON.stringify({ errcode: 0, errmsg: 'ok' }), { status: 200, headers: { 'Content-Type': 'application/json' }, }); } if (String(url).includes('/queryrecoresultfortext')) { queryCount += 1; if (queryCount === 1) { return new Response(JSON.stringify({ errcode: -1, errmsg: 'system error' }), { status: 200, headers: { 'Content-Type': 'application/json' }, }); } return new Response(JSON.stringify({ errcode: 0, errmsg: 'ok', result: '识别完成' }), { status: 200, headers: { 'Content-Type': 'application/json' }, }); } throw new Error(`unexpected url: ${url}`); }, convertToMp3: async () => Buffer.from('fake-mp3'), pollIntervalMs: 1, pollTimeoutMs: 200, }); assert.equal(text, '识别完成'); assert.ok(queryCount >= 2); }); test('queryWechatVoiceRecoResult returns trimmed result', async () => { const result = await queryWechatVoiceRecoResult({ accessToken: 'token-1', voiceId: 'voice-1', wechatFetch: async () => new Response(JSON.stringify({ errcode: 0, errmsg: 'ok', result: ' 你好 ' }), { status: 200, headers: { 'Content-Type': 'application/json' }, }), }); assert.equal(result, '你好'); });