Files
memind/wechat-voice-reco.test.mjs
john 26846a0274
Memind CI / Test, build, and release guards (push) Successful in 3m27s
feat(wechat): add WeChat voice reco API fallback for service account
When passive Recognition is empty, convert AMR to mp3 and call WeChat
addvoicetorecofortext before the existing asr.tkmind.cn fallback.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 18:01:50 +08:00

139 lines
4.5 KiB
JavaScript

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, '你好');
});