Files
WechatExplorer/tests/unit/voice-message-reference.test.ts
T
Wxw-Gu 6192e7cd35 feat: 完善群聊日报语音转写缓存
支持空内容的微信语音消息并兼容历史缓存转写结果
将系统通知标记为微信系统消息,排除活跃成员与发言排行
2026-08-11 11:08:55 +08:00

155 lines
4.9 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest'
import type { Message } from '../../src/shared/types'
import type { VoiceModelStatus } from '../../src/shared/voice-recognition'
import {
toVoiceMessageReference,
transcribeVoiceMessages,
type VoiceTranscriptionProgress
} from '../../src/renderer/src/utils/voice-message-reference'
const status = (state: VoiceModelStatus['state']): VoiceModelStatus =>
({
modelId: 'fixture',
version: '1',
state,
downloadedBytes: 1,
totalBytes: 1,
progress: 1,
platform: 'win32',
architecture: 'x64',
supported: true
}) as VoiceModelStatus
const voice = (id: string, localId: number, transcript?: string): Message => ({
id,
from: 'member',
type: '语音',
datetime: '2026-08-10 10:00:00',
content: '[语音]',
isSender: false,
sessionId: 'session',
localId,
createTime: 1_786_320_000,
contentData: { type: 'voice', duration: 2 },
voiceTranscript: transcript
})
describe('daily report voice transcription', () => {
it('creates a local voice reference from a parsed message', () => {
expect(toVoiceMessageReference(voice('voice-1', 7))).toEqual({
sessionId: 'session',
localId: 7,
createTime: 1_786_320_000,
svrId: undefined
})
})
it('reuses cached text and reports success/failure progress', async () => {
const recognize = vi
.fn()
.mockResolvedValueOnce({ success: true, transcript: '新转写内容' })
.mockResolvedValueOnce({ success: false, error: '音频缺失' })
const onProgress = vi.fn<(progress: VoiceTranscriptionProgress) => void>()
const result = await transcribeVoiceMessages(
[voice('cached', 1, '缓存内容'), voice('fresh', 2), voice('failed', 3)],
{
getModelStatus: vi.fn(async () => status('ready')),
recognize,
onProgress
}
)
expect(result.map((message) => message.voiceTranscript)).toEqual([
'缓存内容',
'新转写内容',
undefined
])
expect(result[2].voiceTranscriptError).toBe('音频缺失')
expect(recognize).toHaveBeenCalledTimes(2)
expect(onProgress).toHaveBeenLastCalledWith({
processed: 3,
total: 3,
succeeded: 2,
failed: 1
})
})
it('normalizes legacy voice messages without contentData for report facts', async () => {
const legacy = { ...voice('legacy', 4), contentData: undefined }
const result = await transcribeVoiceMessages([legacy], {
getModelStatus: vi.fn(async () => status('ready')),
recognize: vi.fn(async () => ({ success: true, transcript: '日报语音内容' })),
onProgress: vi.fn()
})
expect(result[0]).toMatchObject({
voiceTranscript: '日报语音内容',
contentData: { type: 'voice' }
})
})
it('does not require the model when every transcript is cached', async () => {
const getModelStatus = vi.fn(async () => status('missing'))
const recognize = vi.fn()
const result = await transcribeVoiceMessages([voice('cached', 1, '已有文本')], {
getModelStatus,
recognize,
onProgress: vi.fn()
})
expect(result[0].voiceTranscript).toBe('已有文本')
expect(getModelStatus).not.toHaveBeenCalled()
expect(recognize).not.toHaveBeenCalled()
})
it('hydrates persisted transcript cache before invoking recognition', async () => {
const getModelStatus = vi.fn(async () => status('missing'))
const getCachedTranscript = vi.fn(async () => ({
state: 'transcribed' as const,
transcript: '持久化缓存内容'
}))
const recognize = vi.fn()
const result = await transcribeVoiceMessages([voice('persisted', 8)], {
getModelStatus,
getCachedTranscript,
recognize,
onProgress: vi.fn()
})
expect(result[0].voiceTranscript).toBe('持久化缓存内容')
expect(getCachedTranscript).toHaveBeenCalledOnce()
expect(getModelStatus).not.toHaveBeenCalled()
expect(recognize).not.toHaveBeenCalled()
})
it('stops before recognition when the local model is not ready', async () => {
const recognize = vi.fn()
await expect(
transcribeVoiceMessages([voice('pending', 1)], {
getModelStatus: vi.fn(async () => status('missing')),
recognize,
onProgress: vi.fn()
})
).rejects.toThrow('准备离线语音识别模型')
expect(recognize).not.toHaveBeenCalled()
})
it('counts a voice message with incomplete local identifiers as failed', async () => {
const incomplete = { ...voice('incomplete', 1), sessionId: undefined }
const onProgress = vi.fn<(progress: VoiceTranscriptionProgress) => void>()
const result = await transcribeVoiceMessages([incomplete], {
getModelStatus: vi.fn(async () => status('ready')),
recognize: vi.fn(),
onProgress
})
expect(result[0].voiceTranscriptError).toContain('标识不完整')
expect(onProgress).toHaveBeenLastCalledWith({
processed: 1,
total: 1,
succeeded: 0,
failed: 1
})
})
})