Files
WechatExplorer/tests/unit/voice-batch-service.test.ts
T

193 lines
6.8 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { VoiceRecognitionUseCase } from '../../src/main/voice-pipeline/voice-recognition-use-case'
const { countVoiceMessagesAsync, listContactsAsync, listMessagesAsync } = vi.hoisted(() => ({
countVoiceMessagesAsync: vi.fn(),
listContactsAsync: vi.fn(),
listMessagesAsync: vi.fn()
}))
vi.mock('../../src/main/services/chat-service', () => ({
countVoiceMessagesAsync,
listContactsAsync,
listMessagesAsync
}))
import { VoiceBatchService } from '../../src/main/voice-pipeline/voice-batch-service'
const readyStatus = {
modelId: 'sensevoice-small-int8',
version: 'fixture',
state: 'ready' as const,
downloadedBytes: 1,
totalBytes: 1,
progress: 1,
platform: 'win32' as const,
architecture: 'x64',
supported: true
}
function makeRecognition(): VoiceRecognitionUseCase {
return {
accountIdentity: 'account-a',
getModelStatus: vi.fn().mockResolvedValue(readyStatus),
getTranscriptSnapshot: vi.fn().mockReturnValue({ state: 'pending' }),
recognize: vi.fn().mockResolvedValue({ success: true, transcript: '转写结果', cached: false }),
publishTranscriptSnapshot: vi.fn().mockResolvedValue(undefined)
} as unknown as VoiceRecognitionUseCase
}
describe('VoiceBatchService', () => {
beforeEach(() => {
listContactsAsync.mockReset()
listMessagesAsync.mockReset()
countVoiceMessagesAsync.mockReset()
listContactsAsync.mockResolvedValue([
{ md5: 'contact-a', m_nsUsrName: 'contact-a-id', m_nsNickName: '联系人 A', type: 'user' },
{ md5: 'group-b', m_nsUsrName: 'group-b@chatroom', m_nsNickName: '群聊 B', type: 'group' }
])
listMessagesAsync.mockImplementation(async (conversationId: string) => [
{
id: `${conversationId}-voice`,
type: '语音',
content: '[语音消息]',
isSender: false,
sessionId: conversationId === 'contact-a' ? 'contact-a-id' : 'group-b@chatroom',
localId: conversationId === 'contact-a' ? 11 : 22,
createTime: 1_785_895_200
},
{
id: `${conversationId}-text`,
type: '普通文本',
content: '不会进入语音任务',
isSender: false,
createTime: 1_785_895_201
}
])
countVoiceMessagesAsync.mockImplementation(async (conversationId: string) =>
conversationId === 'contact-a' ? 3 : 7
)
})
it('limits a batch to selected contacts and groups, then schedules each item as background work', async () => {
const recognition = makeRecognition()
const service = new VoiceBatchService(recognition)
const preflight = await service.preflight({
conversationIds: ['contact-a', 'group-b'],
range: 'recent_30_days'
})
expect(preflight).toMatchObject({
conversationCount: 2,
voiceMessageCount: 2,
cachedCount: 0,
pendingCount: 2,
modelReady: true
})
expect(listMessagesAsync).toHaveBeenCalledTimes(2)
expect(listMessagesAsync.mock.calls[0][1]).toEqual(expect.any(Number))
await service.start({ conversationIds: ['contact-a'], range: 'selected_history' })
await vi.waitFor(() => expect(service.getProgress().state).toBe('completed'))
expect(recognition.recognize).toHaveBeenCalledWith(
expect.objectContaining({ sessionId: 'contact-a-id', localId: 11 }),
{ priority: 'background', publishTranscriptUpdate: false }
)
expect(recognition.recognize).toHaveBeenCalledTimes(1)
await vi.waitFor(() =>
expect(recognition.publishTranscriptSnapshot).toHaveBeenCalledWith(
expect.objectContaining({ sessionId: 'contact-a-id', localId: 11 })
)
)
})
it('defers knowledge notifications until the batch finishes and sends one per conversation', async () => {
listMessagesAsync.mockResolvedValue([
{
id: 'contact-a-voice-1',
type: '语音',
content: '[语音消息]',
isSender: false,
sessionId: 'contact-a-id',
localId: 11,
createTime: 1_785_895_200
},
{
id: 'contact-a-voice-2',
type: '语音',
content: '[语音消息]',
isSender: false,
sessionId: 'contact-a-id',
localId: 12,
createTime: 1_785_895_201
}
])
const recognition = makeRecognition()
let releaseKnowledgeRefresh: (() => void) | undefined
vi.mocked(recognition.publishTranscriptSnapshot).mockImplementationOnce(
() =>
new Promise<void>((resolve) => {
releaseKnowledgeRefresh = resolve
})
)
const service = new VoiceBatchService(recognition)
await service.start({ conversationIds: ['contact-a'], range: 'selected_history' })
await vi.waitFor(() => expect(recognition.publishTranscriptSnapshot).toHaveBeenCalledTimes(1))
expect(service.getProgress().state).toBe('processing')
releaseKnowledgeRefresh?.()
await vi.waitFor(() => expect(service.getProgress().state).toBe('completed'))
expect(recognition.recognize).toHaveBeenCalledTimes(2)
expect(recognition.recognize).toHaveBeenNthCalledWith(
1,
expect.anything(),
expect.objectContaining({ publishTranscriptUpdate: false })
)
expect(recognition.publishTranscriptSnapshot).toHaveBeenCalledTimes(1)
})
it('does not start when the selected range contains a stale conversation id', async () => {
const service = new VoiceBatchService(makeRecognition())
await expect(
service.preflight({ conversationIds: ['missing-conversation'], range: 'recent_30_days' })
).rejects.toThrow('选择的会话已不可用')
})
it('reports cache hits separately from newly recognized items', async () => {
const recognition = makeRecognition()
vi.mocked(recognition.getTranscriptSnapshot).mockReturnValue({
state: 'transcribed',
transcript: '旧缓存'
})
vi.mocked(recognition.recognize).mockResolvedValue({
success: true,
transcript: '旧缓存',
cached: true
})
const service = new VoiceBatchService(recognition)
await service.start({ conversationIds: ['contact-a'], range: 'selected_history' })
await vi.waitFor(() => expect(service.getProgress().state).toBe('completed'))
expect(service.getProgress()).toMatchObject({ cached: 1, succeeded: 0, failed: 0 })
})
it('counts visible conversations through the lightweight voice-count path without loading messages', async () => {
const service = new VoiceBatchService(makeRecognition())
await expect(
service.conversationSummaries({
conversationIds: ['contact-a', 'group-b'],
range: 'recent_30_days'
})
).resolves.toEqual([
{ conversationId: 'contact-a', voiceMessageCount: 3 },
{ conversationId: 'group-b', voiceMessageCount: 7 }
])
expect(countVoiceMessagesAsync).toHaveBeenCalledTimes(2)
expect(listMessagesAsync).not.toHaveBeenCalled()
})
})