feat: initial release v0.3.0
This commit is contained in:
78
tests/unit/providers/bailian-llm.test.ts
Normal file
78
tests/unit/providers/bailian-llm.test.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const createChatCompletionMock = vi.hoisted(() =>
|
||||
vi.fn(async () => ({
|
||||
id: 'chatcmpl_bailian',
|
||||
object: 'chat.completion',
|
||||
created: 1,
|
||||
model: 'qwen3.5-plus',
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
message: { role: 'assistant', content: 'ok' },
|
||||
finish_reason: 'stop',
|
||||
},
|
||||
],
|
||||
usage: {
|
||||
prompt_tokens: 1,
|
||||
completion_tokens: 1,
|
||||
total_tokens: 2,
|
||||
},
|
||||
})),
|
||||
)
|
||||
|
||||
const openAiCtorMock = vi.hoisted(() =>
|
||||
vi.fn(() => ({
|
||||
chat: {
|
||||
completions: {
|
||||
create: createChatCompletionMock,
|
||||
},
|
||||
},
|
||||
})),
|
||||
)
|
||||
|
||||
vi.mock('openai', () => ({
|
||||
default: openAiCtorMock,
|
||||
}))
|
||||
|
||||
import { completeBailianLlm } from '@/lib/providers/bailian/llm'
|
||||
|
||||
describe('bailian llm provider', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('calls dashscope openai-compatible endpoint for registered qwen model', async () => {
|
||||
const completion = await completeBailianLlm({
|
||||
modelId: 'qwen3.5-plus',
|
||||
messages: [{ role: 'user', content: 'hello' }],
|
||||
apiKey: 'bl-key',
|
||||
temperature: 0.2,
|
||||
})
|
||||
|
||||
expect(openAiCtorMock).toHaveBeenCalledWith({
|
||||
apiKey: 'bl-key',
|
||||
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
||||
timeout: 30_000,
|
||||
})
|
||||
expect(createChatCompletionMock).toHaveBeenCalledWith({
|
||||
model: 'qwen3.5-plus',
|
||||
messages: [{ role: 'user', content: 'hello' }],
|
||||
temperature: 0.2,
|
||||
})
|
||||
expect(completion.choices[0]?.message?.content).toBe('ok')
|
||||
})
|
||||
|
||||
it('fails fast when model is not in official bailian catalog', async () => {
|
||||
await expect(
|
||||
completeBailianLlm({
|
||||
modelId: 'qwen-plus',
|
||||
messages: [{ role: 'user', content: 'hello' }],
|
||||
apiKey: 'bl-key',
|
||||
}),
|
||||
).rejects.toThrow(/MODEL_NOT_REGISTERED/)
|
||||
|
||||
expect(openAiCtorMock).not.toHaveBeenCalled()
|
||||
expect(createChatCompletionMock).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
145
tests/unit/providers/bailian-tts.test.ts
Normal file
145
tests/unit/providers/bailian-tts.test.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { synthesizeWithBailianTTS } from '@/lib/providers/bailian/tts'
|
||||
|
||||
function buildWavBuffer(durationMs: number): Buffer {
|
||||
const sampleRate = 8000
|
||||
const channels = 1
|
||||
const bitsPerSample = 16
|
||||
const byteRate = sampleRate * channels * (bitsPerSample / 8)
|
||||
const dataLength = Math.round((durationMs / 1000) * byteRate)
|
||||
const pcmData = Buffer.alloc(dataLength, 0)
|
||||
const output = Buffer.alloc(44 + dataLength)
|
||||
|
||||
output.write('RIFF', 0, 'ascii')
|
||||
output.writeUInt32LE(36 + dataLength, 4)
|
||||
output.write('WAVE', 8, 'ascii')
|
||||
output.write('fmt ', 12, 'ascii')
|
||||
output.writeUInt32LE(16, 16)
|
||||
output.writeUInt16LE(1, 20)
|
||||
output.writeUInt16LE(channels, 22)
|
||||
output.writeUInt32LE(sampleRate, 24)
|
||||
output.writeUInt32LE(byteRate, 28)
|
||||
output.writeUInt16LE(channels * (bitsPerSample / 8), 32)
|
||||
output.writeUInt16LE(bitsPerSample, 34)
|
||||
output.write('data', 36, 'ascii')
|
||||
output.writeUInt32LE(dataLength, 40)
|
||||
pcmData.copy(output, 44)
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
describe('bailian tts synthesis', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('synthesizes one segment and returns wav buffer', async () => {
|
||||
const wav = buildWavBuffer(120)
|
||||
const fetchMock = vi.fn(async (input: string) => {
|
||||
if (input.includes('/multimodal-generation/generation')) {
|
||||
return {
|
||||
ok: true,
|
||||
text: async () => JSON.stringify({
|
||||
output: {
|
||||
audio: {
|
||||
url: 'https://audio.example/segment-1.wav',
|
||||
},
|
||||
},
|
||||
usage: { characters: 10 },
|
||||
request_id: 'req-1',
|
||||
}),
|
||||
}
|
||||
}
|
||||
if (input === 'https://audio.example/segment-1.wav') {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
arrayBuffer: async () => wav.buffer.slice(wav.byteOffset, wav.byteOffset + wav.byteLength),
|
||||
}
|
||||
}
|
||||
throw new Error(`unexpected fetch url: ${input}`)
|
||||
})
|
||||
vi.stubGlobal('fetch', fetchMock as unknown as typeof fetch)
|
||||
|
||||
const result = await synthesizeWithBailianTTS({
|
||||
text: '你好,世界',
|
||||
voiceId: 'voice_1',
|
||||
}, 'bl-key')
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.audioData).toBeDefined()
|
||||
expect(result.audioDuration).toBe(120)
|
||||
expect(result.audioUrl).toBe('https://audio.example/segment-1.wav')
|
||||
expect(result.characters).toBe(10)
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('splits text over 600 chars and merges audio segments', async () => {
|
||||
const wavA = buildWavBuffer(100)
|
||||
const wavB = buildWavBuffer(200)
|
||||
let generationCallCount = 0
|
||||
const fetchMock = vi.fn(async (input: string) => {
|
||||
if (input.includes('/multimodal-generation/generation')) {
|
||||
generationCallCount += 1
|
||||
const audioUrl = generationCallCount === 1
|
||||
? 'https://audio.example/segment-a.wav'
|
||||
: 'https://audio.example/segment-b.wav'
|
||||
return {
|
||||
ok: true,
|
||||
text: async () => JSON.stringify({
|
||||
output: {
|
||||
audio: { url: audioUrl },
|
||||
},
|
||||
usage: { characters: 600 },
|
||||
request_id: `req-${generationCallCount}`,
|
||||
}),
|
||||
}
|
||||
}
|
||||
if (input === 'https://audio.example/segment-a.wav') {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
arrayBuffer: async () => wavA.buffer.slice(wavA.byteOffset, wavA.byteOffset + wavA.byteLength),
|
||||
}
|
||||
}
|
||||
if (input === 'https://audio.example/segment-b.wav') {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
arrayBuffer: async () => wavB.buffer.slice(wavB.byteOffset, wavB.byteOffset + wavB.byteLength),
|
||||
}
|
||||
}
|
||||
throw new Error(`unexpected fetch url: ${input}`)
|
||||
})
|
||||
vi.stubGlobal('fetch', fetchMock as unknown as typeof fetch)
|
||||
|
||||
const result = await synthesizeWithBailianTTS({
|
||||
text: 'a'.repeat(601),
|
||||
voiceId: 'voice_2',
|
||||
}, 'bl-key')
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.audioData).toBeDefined()
|
||||
expect(result.audioDuration).toBe(300)
|
||||
expect(result.audioUrl).toBeUndefined()
|
||||
expect(result.characters).toBe(1200)
|
||||
expect(generationCallCount).toBe(2)
|
||||
})
|
||||
|
||||
it('fails explicitly when voiceId is missing', async () => {
|
||||
const fetchMock = vi.fn()
|
||||
vi.stubGlobal('fetch', fetchMock as unknown as typeof fetch)
|
||||
|
||||
const result = await synthesizeWithBailianTTS({
|
||||
text: 'hello',
|
||||
voiceId: '',
|
||||
}, 'bl-key')
|
||||
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
error: 'BAILIAN_TTS_VOICE_ID_REQUIRED',
|
||||
})
|
||||
expect(fetchMock).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
179
tests/unit/providers/bailian-video.test.ts
Normal file
179
tests/unit/providers/bailian-video.test.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const getProviderConfigMock = vi.hoisted(() =>
|
||||
vi.fn(async () => ({
|
||||
id: 'bailian',
|
||||
apiKey: 'bl-key',
|
||||
})),
|
||||
)
|
||||
|
||||
vi.mock('@/lib/api-config', () => ({
|
||||
getProviderConfig: getProviderConfigMock,
|
||||
}))
|
||||
|
||||
import { generateBailianVideo } from '@/lib/providers/bailian/video'
|
||||
|
||||
describe('bailian video provider', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('submits i2v task and returns async externalId', async () => {
|
||||
const fetchMock = vi.fn(async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: async () => JSON.stringify({
|
||||
request_id: 'req-1',
|
||||
output: {
|
||||
task_id: 'task-123',
|
||||
task_status: 'PENDING',
|
||||
},
|
||||
}),
|
||||
}))
|
||||
vi.stubGlobal('fetch', fetchMock as unknown as typeof fetch)
|
||||
|
||||
const result = await generateBailianVideo({
|
||||
userId: 'user-1',
|
||||
imageUrl: 'https://example.com/frame.png',
|
||||
prompt: '让人物向前走',
|
||||
options: {
|
||||
provider: 'bailian',
|
||||
modelId: 'wan2.6-i2v-flash',
|
||||
modelKey: 'bailian::wan2.6-i2v-flash',
|
||||
duration: 5,
|
||||
resolution: '720P',
|
||||
promptExtend: true,
|
||||
},
|
||||
})
|
||||
|
||||
expect(getProviderConfigMock).toHaveBeenCalledWith('user-1', 'bailian')
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1)
|
||||
const firstCall = fetchMock.mock.calls[0] as unknown as [RequestInfo | URL, RequestInit] | undefined
|
||||
expect(firstCall).toBeDefined()
|
||||
if (!firstCall) {
|
||||
throw new Error('missing fetch call')
|
||||
}
|
||||
const requestUrl = firstCall[0]
|
||||
const requestInit = firstCall[1]
|
||||
expect(requestUrl).toBe('https://dashscope.aliyuncs.com/api/v1/services/aigc/video-generation/video-synthesis')
|
||||
expect(requestInit.method).toBe('POST')
|
||||
expect(requestInit.headers).toEqual({
|
||||
Authorization: 'Bearer bl-key',
|
||||
'Content-Type': 'application/json',
|
||||
'X-DashScope-Async': 'enable',
|
||||
})
|
||||
expect(requestInit.body).toBe(JSON.stringify({
|
||||
model: 'wan2.6-i2v-flash',
|
||||
input: {
|
||||
img_url: 'https://example.com/frame.png',
|
||||
prompt: '让人物向前走',
|
||||
},
|
||||
parameters: {
|
||||
resolution: '720P',
|
||||
prompt_extend: true,
|
||||
duration: 5,
|
||||
},
|
||||
}))
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
async: true,
|
||||
requestId: 'task-123',
|
||||
externalId: 'BAILIAN:VIDEO:task-123',
|
||||
})
|
||||
})
|
||||
|
||||
it('submits kf2v task with first and last frame', async () => {
|
||||
const fetchMock = vi.fn(async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: async () => JSON.stringify({
|
||||
output: {
|
||||
task_id: 'task-kf2v-1',
|
||||
task_status: 'PENDING',
|
||||
},
|
||||
}),
|
||||
}))
|
||||
vi.stubGlobal('fetch', fetchMock as unknown as typeof fetch)
|
||||
|
||||
const result = await generateBailianVideo({
|
||||
userId: 'user-1',
|
||||
imageUrl: 'https://example.com/first.png',
|
||||
prompt: '让人物从左走到右',
|
||||
options: {
|
||||
provider: 'bailian',
|
||||
modelId: 'wan2.2-kf2v-flash',
|
||||
modelKey: 'bailian::wan2.2-kf2v-flash',
|
||||
lastFrameImageUrl: 'https://example.com/last.png',
|
||||
duration: 5,
|
||||
},
|
||||
})
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1)
|
||||
const firstCall = fetchMock.mock.calls[0] as unknown as [RequestInfo | URL, RequestInit] | undefined
|
||||
expect(firstCall).toBeDefined()
|
||||
if (!firstCall) {
|
||||
throw new Error('missing fetch call')
|
||||
}
|
||||
const requestUrl = firstCall[0]
|
||||
const requestInit = firstCall[1]
|
||||
expect(requestUrl).toBe('https://dashscope.aliyuncs.com/api/v1/services/aigc/image2video/video-synthesis')
|
||||
expect(requestInit.body).toBe(JSON.stringify({
|
||||
model: 'wan2.2-kf2v-flash',
|
||||
input: {
|
||||
first_frame_url: 'https://example.com/first.png',
|
||||
last_frame_url: 'https://example.com/last.png',
|
||||
prompt: '让人物从左走到右',
|
||||
},
|
||||
parameters: {
|
||||
duration: 5,
|
||||
},
|
||||
}))
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
async: true,
|
||||
requestId: 'task-kf2v-1',
|
||||
externalId: 'BAILIAN:VIDEO:task-kf2v-1',
|
||||
})
|
||||
})
|
||||
|
||||
it('fails fast when kf2v model misses last frame', async () => {
|
||||
const fetchMock = vi.fn()
|
||||
vi.stubGlobal('fetch', fetchMock as unknown as typeof fetch)
|
||||
|
||||
await expect(
|
||||
generateBailianVideo({
|
||||
userId: 'user-1',
|
||||
imageUrl: 'https://example.com/first.png',
|
||||
prompt: 'test',
|
||||
options: {
|
||||
provider: 'bailian',
|
||||
modelId: 'wanx2.1-kf2v-plus',
|
||||
modelKey: 'bailian::wanx2.1-kf2v-plus',
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow(/BAILIAN_VIDEO_LAST_FRAME_IMAGE_URL_REQUIRED/)
|
||||
|
||||
expect(fetchMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('fails fast when options contain unsupported field', async () => {
|
||||
const fetchMock = vi.fn()
|
||||
vi.stubGlobal('fetch', fetchMock as unknown as typeof fetch)
|
||||
|
||||
await expect(
|
||||
generateBailianVideo({
|
||||
userId: 'user-1',
|
||||
imageUrl: 'https://example.com/frame.png',
|
||||
prompt: 'test',
|
||||
options: {
|
||||
provider: 'bailian',
|
||||
modelId: 'wan2.6-i2v',
|
||||
modelKey: 'bailian::wan2.6-i2v',
|
||||
fps: 24,
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow(/BAILIAN_VIDEO_OPTION_UNSUPPORTED: fps/)
|
||||
|
||||
expect(fetchMock).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
118
tests/unit/providers/bailian-voice-cleanup.test.ts
Normal file
118
tests/unit/providers/bailian-voice-cleanup.test.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const prismaMock = vi.hoisted(() => ({
|
||||
novelPromotionProject: {
|
||||
findUnique: vi.fn(),
|
||||
},
|
||||
novelPromotionCharacter: {
|
||||
findMany: vi.fn(),
|
||||
},
|
||||
globalCharacter: {
|
||||
findMany: vi.fn(),
|
||||
},
|
||||
globalVoice: {
|
||||
findMany: vi.fn(),
|
||||
},
|
||||
novelPromotionEpisode: {
|
||||
findMany: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
const getProviderConfigMock = vi.hoisted(() => vi.fn())
|
||||
const deleteBailianVoiceMock = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('@/lib/prisma', () => ({
|
||||
prisma: prismaMock,
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/api-config', () => ({
|
||||
getProviderConfig: getProviderConfigMock,
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/providers/bailian/voice-manage', () => ({
|
||||
deleteBailianVoice: deleteBailianVoiceMock,
|
||||
}))
|
||||
|
||||
import {
|
||||
collectBailianManagedVoiceIds,
|
||||
collectProjectBailianManagedVoiceIds,
|
||||
cleanupUnreferencedBailianVoices,
|
||||
isBailianManagedVoiceBinding,
|
||||
} from '@/lib/providers/bailian/voice-cleanup'
|
||||
|
||||
describe('bailian voice cleanup', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
prismaMock.novelPromotionCharacter.findMany.mockResolvedValue([])
|
||||
prismaMock.globalCharacter.findMany.mockResolvedValue([])
|
||||
prismaMock.globalVoice.findMany.mockResolvedValue([])
|
||||
prismaMock.novelPromotionEpisode.findMany.mockResolvedValue([])
|
||||
getProviderConfigMock.mockResolvedValue({
|
||||
apiKey: 'bl-key',
|
||||
})
|
||||
deleteBailianVoiceMock.mockResolvedValue({ requestId: 'req-1' })
|
||||
})
|
||||
|
||||
it('identifies managed voice bindings by voiceType or id prefix', () => {
|
||||
expect(isBailianManagedVoiceBinding({ voiceType: 'qwen-designed', voiceId: 'voice-a' })).toBe(true)
|
||||
expect(isBailianManagedVoiceBinding({ voiceType: 'uploaded', voiceId: 'qwen-tts-vd-voice-b' })).toBe(true)
|
||||
expect(isBailianManagedVoiceBinding({ voiceType: 'uploaded', voiceId: 'custom-voice-b' })).toBe(false)
|
||||
})
|
||||
|
||||
it('collects and deduplicates managed voice ids', () => {
|
||||
const voiceIds = collectBailianManagedVoiceIds([
|
||||
{ voiceType: 'qwen-designed', voiceId: 'qwen-tts-vd-1' },
|
||||
{ voiceType: 'qwen-designed', voiceId: 'qwen-tts-vd-1' },
|
||||
{ voiceType: 'uploaded', voiceId: 'custom-1' },
|
||||
{ voiceType: null, voiceId: 'qwen-tts-vd-2' },
|
||||
])
|
||||
|
||||
expect(voiceIds).toEqual(['qwen-tts-vd-1', 'qwen-tts-vd-2'])
|
||||
})
|
||||
|
||||
it('deletes only unreferenced managed voices', async () => {
|
||||
prismaMock.globalVoice.findMany.mockResolvedValue([
|
||||
{ voiceId: 'qwen-tts-vd-1' },
|
||||
])
|
||||
|
||||
const result = await cleanupUnreferencedBailianVoices({
|
||||
voiceIds: ['qwen-tts-vd-1', 'qwen-tts-vd-2'],
|
||||
scope: {
|
||||
userId: 'user-1',
|
||||
},
|
||||
})
|
||||
|
||||
expect(getProviderConfigMock).toHaveBeenCalledWith('user-1', 'bailian')
|
||||
expect(deleteBailianVoiceMock).toHaveBeenCalledTimes(1)
|
||||
expect(deleteBailianVoiceMock).toHaveBeenCalledWith({
|
||||
apiKey: 'bl-key',
|
||||
voiceId: 'qwen-tts-vd-2',
|
||||
})
|
||||
expect(result).toEqual({
|
||||
requestedVoiceIds: ['qwen-tts-vd-1', 'qwen-tts-vd-2'],
|
||||
skippedReferencedVoiceIds: ['qwen-tts-vd-1'],
|
||||
deletedVoiceIds: ['qwen-tts-vd-2'],
|
||||
})
|
||||
})
|
||||
|
||||
it('collects managed voice ids from project characters and speaker voices', async () => {
|
||||
prismaMock.novelPromotionProject.findUnique.mockResolvedValue({
|
||||
characters: [
|
||||
{ voiceId: 'qwen-tts-vd-character', voiceType: 'qwen-designed' },
|
||||
{ voiceId: 'plain-custom', voiceType: 'uploaded' },
|
||||
],
|
||||
episodes: [
|
||||
{
|
||||
speakerVoices: JSON.stringify({
|
||||
Narrator: { voiceType: 'qwen-designed', voiceId: 'qwen-tts-vd-inline' },
|
||||
Guest: { voiceType: 'uploaded', voiceId: 'uploaded-id' },
|
||||
}),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const voiceIds = await collectProjectBailianManagedVoiceIds('project-1')
|
||||
expect(voiceIds).toEqual(['qwen-tts-vd-character', 'qwen-tts-vd-inline'])
|
||||
})
|
||||
})
|
||||
|
||||
53
tests/unit/providers/bailian-voice-design.test.ts
Normal file
53
tests/unit/providers/bailian-voice-design.test.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createVoiceDesign } from '@/lib/providers/bailian/voice-design'
|
||||
|
||||
describe('bailian voice design', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('uses qwen3-tts-vd-2026-01-26 as target model', async () => {
|
||||
const fetchMock = vi.fn(async (_input: unknown, _init?: unknown) => ({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
output: {
|
||||
voice: 'voice_1',
|
||||
target_model: 'qwen3-tts-vd-2026-01-26',
|
||||
preview_audio: {
|
||||
data: 'base64',
|
||||
sample_rate: 24000,
|
||||
response_format: 'wav',
|
||||
},
|
||||
},
|
||||
usage: { count: 1 },
|
||||
request_id: 'req-1',
|
||||
}),
|
||||
text: async () => '',
|
||||
status: 200,
|
||||
headers: new Headers(),
|
||||
redirected: false,
|
||||
type: 'basic',
|
||||
url: '',
|
||||
bodyUsed: false,
|
||||
clone: () => undefined as unknown as Response,
|
||||
body: null,
|
||||
arrayBuffer: async () => new ArrayBuffer(0),
|
||||
blob: async () => new Blob(),
|
||||
formData: async () => new FormData(),
|
||||
}))
|
||||
vi.stubGlobal('fetch', fetchMock as unknown as typeof fetch)
|
||||
|
||||
await createVoiceDesign({
|
||||
voicePrompt: '成熟稳重男声',
|
||||
previewText: '你好测试',
|
||||
}, 'bl-key')
|
||||
|
||||
const firstCall = fetchMock.mock.calls[0] as [unknown, RequestInit?] | undefined
|
||||
const requestBodyRaw = firstCall?.[1]?.body
|
||||
expect(typeof requestBodyRaw).toBe('string')
|
||||
const requestBody = JSON.parse(requestBodyRaw as string) as {
|
||||
input?: { target_model?: string }
|
||||
}
|
||||
expect(requestBody.input?.target_model).toBe('qwen3-tts-vd-2026-01-26')
|
||||
})
|
||||
})
|
||||
39
tests/unit/providers/model-registry.test.ts
Normal file
39
tests/unit/providers/model-registry.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
assertOfficialModelRegistered,
|
||||
isOfficialModelRegistered,
|
||||
registerOfficialModel,
|
||||
resetOfficialModelRegistryForTest,
|
||||
} from '@/lib/providers/official/model-registry'
|
||||
|
||||
describe('official model registry', () => {
|
||||
beforeEach(() => {
|
||||
resetOfficialModelRegistryForTest()
|
||||
})
|
||||
|
||||
it('throws MODEL_NOT_REGISTERED when model is absent', () => {
|
||||
expect(() =>
|
||||
assertOfficialModelRegistered({
|
||||
provider: 'bailian',
|
||||
modality: 'llm',
|
||||
modelId: 'qwen-plus',
|
||||
}),
|
||||
).toThrow(/MODEL_NOT_REGISTERED/)
|
||||
})
|
||||
|
||||
it('accepts registered official model', () => {
|
||||
registerOfficialModel({
|
||||
provider: 'siliconflow',
|
||||
modality: 'image',
|
||||
modelId: 'sf-image',
|
||||
})
|
||||
|
||||
expect(
|
||||
isOfficialModelRegistered({
|
||||
provider: 'siliconflow',
|
||||
modality: 'image',
|
||||
modelId: 'sf-image',
|
||||
}),
|
||||
).toBe(true)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user