Skip to content

Commit a5126a8

Browse files
NianJiuZstclaude
andcommitted
test: add unit tests for speech voices command
Cover voice listing, language filtering (exact, dialect, multi-word, case-insensitive), dry-run, JSON output, and the filterByLanguage utility function. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c388d5e commit a5126a8

1 file changed

Lines changed: 232 additions & 0 deletions

File tree

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
import { describe, it, expect, afterEach } from 'bun:test';
2+
import { createMockServer, jsonResponse, type MockServer } from '../../helpers/mock-server';
3+
import { default as voicesCommand } from '../../../src/commands/speech/voices';
4+
import { filterByLanguage } from '../../../src/commands/speech/voices';
5+
import type { SystemVoiceInfo } from '../../../src/types/api';
6+
7+
const baseConfig = {
8+
apiKey: 'test-key',
9+
region: 'global' as const,
10+
baseUrl: 'https://api.mmx.io',
11+
output: 'text' as const,
12+
timeout: 10,
13+
verbose: false,
14+
quiet: false,
15+
noColor: true,
16+
yes: false,
17+
dryRun: false,
18+
nonInteractive: true,
19+
async: false,
20+
};
21+
22+
const baseFlags = {
23+
quiet: false,
24+
verbose: false,
25+
noColor: true,
26+
yes: false,
27+
dryRun: false,
28+
help: false,
29+
nonInteractive: true,
30+
async: false,
31+
};
32+
33+
const MOCK_VOICES: SystemVoiceInfo[] = [
34+
{ voice_id: 'English_Narrator', voice_name: 'Narrator', description: ['Clear narration voice'] },
35+
{ voice_id: 'English_Female', voice_name: 'Female', description: ['Warm female voice'] },
36+
{ voice_id: 'Korean_Female', voice_name: 'Korean Female', description: ['Natural Korean'] },
37+
{ voice_id: 'Japanese (Kansai)_Male', voice_name: 'Kansai Male', description: ['Kansai dialect'] },
38+
{ voice_id: 'Chinese_Mandarin_Female', voice_name: 'Mandarin Female', description: ['Standard Mandarin'] },
39+
];
40+
41+
describe('speech voices command', () => {
42+
it('has correct name', () => {
43+
expect(voicesCommand.name).toBe('speech voices');
44+
});
45+
46+
it('has --language option', () => {
47+
const flags = voicesCommand.options?.map(o => o.flag) ?? [];
48+
expect(flags.some(f => f.startsWith('--language'))).toBe(true);
49+
});
50+
51+
it('has examples', () => {
52+
expect(voicesCommand.examples?.length).toBeGreaterThan(0);
53+
});
54+
55+
it('dry-run prints expected request', async () => {
56+
let captured = '';
57+
const origLog = console.log;
58+
console.log = (msg: string) => { captured += msg; };
59+
60+
await voicesCommand.execute(
61+
{ ...baseConfig, dryRun: true, output: 'json' as const },
62+
{ ...baseFlags, dryRun: true },
63+
);
64+
65+
console.log = origLog;
66+
const parsed = JSON.parse(captured);
67+
expect(parsed.request.voice_type).toBe('system');
68+
});
69+
});
70+
71+
describe('speech voices command with mock server', () => {
72+
let server: MockServer;
73+
74+
afterEach(() => {
75+
server?.close();
76+
});
77+
78+
it('lists all voices', async () => {
79+
server = createMockServer({
80+
routes: {
81+
'/v1/get_voice': () => jsonResponse({
82+
system_voice: MOCK_VOICES,
83+
base_resp: { status_code: 0, status_msg: 'ok' },
84+
}),
85+
},
86+
});
87+
88+
let captured = '';
89+
const origLog = console.log;
90+
console.log = (msg: string) => { captured += msg; };
91+
92+
await voicesCommand.execute(
93+
{ ...baseConfig, baseUrl: server.url, output: 'text' as const },
94+
baseFlags,
95+
);
96+
97+
console.log = origLog;
98+
expect(captured).toContain('English_Narrator');
99+
expect(captured).toContain('Korean_Female');
100+
expect(captured).toContain('Chinese_Mandarin_Female');
101+
});
102+
103+
it('filters voices by language', async () => {
104+
server = createMockServer({
105+
routes: {
106+
'/v1/get_voice': () => jsonResponse({
107+
system_voice: MOCK_VOICES,
108+
base_resp: { status_code: 0, status_msg: 'ok' },
109+
}),
110+
},
111+
});
112+
113+
let captured = '';
114+
const origLog = console.log;
115+
console.log = (msg: string) => { captured += msg; };
116+
117+
await voicesCommand.execute(
118+
{ ...baseConfig, baseUrl: server.url, output: 'text' as const },
119+
{ ...baseFlags, language: 'english' },
120+
);
121+
122+
console.log = origLog;
123+
expect(captured).toContain('English_Narrator');
124+
expect(captured).toContain('English_Female');
125+
expect(captured).not.toContain('Korean');
126+
expect(captured).not.toContain('Japanese');
127+
});
128+
129+
it('filters voices by dialect language', async () => {
130+
server = createMockServer({
131+
routes: {
132+
'/v1/get_voice': () => jsonResponse({
133+
system_voice: MOCK_VOICES,
134+
base_resp: { status_code: 0, status_msg: 'ok' },
135+
}),
136+
},
137+
});
138+
139+
let captured = '';
140+
const origLog = console.log;
141+
console.log = (msg: string) => { captured += msg; };
142+
143+
await voicesCommand.execute(
144+
{ ...baseConfig, baseUrl: server.url, output: 'text' as const },
145+
{ ...baseFlags, language: 'japanese' },
146+
);
147+
148+
console.log = origLog;
149+
expect(captured).toContain('Japanese (Kansai)_Male');
150+
expect(captured).not.toContain('English');
151+
expect(captured).not.toContain('Korean');
152+
});
153+
154+
it('returns JSON output when configured', async () => {
155+
server = createMockServer({
156+
routes: {
157+
'/v1/get_voice': () => jsonResponse({
158+
system_voice: MOCK_VOICES,
159+
base_resp: { status_code: 0, status_msg: 'ok' },
160+
}),
161+
},
162+
});
163+
164+
let captured = '';
165+
const origLog = console.log;
166+
console.log = (msg: string) => { captured += msg; };
167+
168+
await voicesCommand.execute(
169+
{ ...baseConfig, baseUrl: server.url, output: 'json' as const },
170+
baseFlags,
171+
);
172+
173+
console.log = origLog;
174+
const parsed = JSON.parse(captured);
175+
expect(Array.isArray(parsed)).toBe(true);
176+
expect(parsed).toContain('English_Narrator');
177+
});
178+
179+
it('JSON output with language filter returns filtered array', async () => {
180+
server = createMockServer({
181+
routes: {
182+
'/v1/get_voice': () => jsonResponse({
183+
system_voice: MOCK_VOICES,
184+
base_resp: { status_code: 0, status_msg: 'ok' },
185+
}),
186+
},
187+
});
188+
189+
let captured = '';
190+
const origLog = console.log;
191+
console.log = (msg: string) => { captured += msg; };
192+
193+
await voicesCommand.execute(
194+
{ ...baseConfig, baseUrl: server.url, output: 'json' as const },
195+
{ ...baseFlags, language: 'korean' },
196+
);
197+
198+
console.log = origLog;
199+
const parsed = JSON.parse(captured);
200+
expect(parsed.length).toBe(1);
201+
expect(parsed[0].voice_id).toBe('Korean_Female');
202+
});
203+
});
204+
205+
describe('filterByLanguage', () => {
206+
it('matches exact language prefix', () => {
207+
const result = filterByLanguage(MOCK_VOICES, 'english');
208+
expect(result.length).toBe(2);
209+
});
210+
211+
it('matches dialect language', () => {
212+
const result = filterByLanguage(MOCK_VOICES, 'japanese');
213+
expect(result.length).toBe(1);
214+
expect(result[0].voice_id).toBe('Japanese (Kansai)_Male');
215+
});
216+
217+
it('returns empty for unmatched language', () => {
218+
const result = filterByLanguage(MOCK_VOICES, 'french');
219+
expect(result.length).toBe(0);
220+
});
221+
222+
it('is case-insensitive', () => {
223+
const result = filterByLanguage(MOCK_VOICES, 'ENGLISH');
224+
expect(result.length).toBe(2);
225+
});
226+
227+
it('matches multi-word language', () => {
228+
const result = filterByLanguage(MOCK_VOICES, 'chinese');
229+
expect(result.length).toBe(1);
230+
expect(result[0].voice_id).toBe('Chinese_Mandarin_Female');
231+
});
232+
});

0 commit comments

Comments
 (0)