Skip to content

Commit 75cb6c4

Browse files
authored
Merge pull request #151 from NianJiuZst/test/missing-command-tests
test: add unit tests for music cover, speech voices, and config export-schema
2 parents 6fa6dbb + 9351000 commit 75cb6c4

3 files changed

Lines changed: 544 additions & 0 deletions

File tree

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { describe, it, expect } from 'bun:test';
2+
import { generateToolSchema } from '../../../src/utils/schema';
3+
import { registry } from '../../../src/registry';
4+
5+
type JsonSchema = Record<string, unknown>;
6+
type ToolSchema = { name: string; description?: string; input_schema: JsonSchema };
7+
8+
function getSchema(cmdName: string): ToolSchema {
9+
const { command } = registry.resolve(cmdName.split(' '));
10+
return generateToolSchema(command) as unknown as ToolSchema;
11+
}
12+
13+
function is(input: unknown): JsonSchema {
14+
return input as JsonSchema;
15+
}
16+
17+
describe('generateToolSchema', () => {
18+
it('generates schema for text chat command', () => {
19+
const schema = getSchema('text chat');
20+
21+
expect(schema.name).toBe('mmx_text_chat');
22+
expect(schema.description).toBeDefined();
23+
expect(schema.input_schema).toBeDefined();
24+
expect(schema.input_schema.type).toBe('object');
25+
expect(schema.input_schema.properties).toBeDefined();
26+
});
27+
28+
it('includes required flag properties', () => {
29+
const schema = getSchema('image generate');
30+
31+
const ischema = schema.input_schema;
32+
const props = is(ischema.properties);
33+
expect(props.prompt).toBeDefined();
34+
expect(is(props.prompt).type).toBe('string');
35+
expect(ischema.required).toContain('prompt');
36+
});
37+
38+
it('infers number type from numeric flags', () => {
39+
const schema = getSchema('text chat');
40+
41+
const props = is(schema.input_schema.properties);
42+
const maxTokens = Object.entries(props).find(([key]) => key === 'maxTokens');
43+
expect(maxTokens).toBeDefined();
44+
expect(is(maxTokens![1]!).type).toBe('number');
45+
});
46+
47+
it('infers boolean type for flagless options', () => {
48+
const schema = getSchema('text chat');
49+
50+
const props = is(schema.input_schema.properties);
51+
expect(props.stream).toBeDefined();
52+
expect(is(props.stream).type).toBe('boolean');
53+
});
54+
55+
it('infers array type for repeatable options', () => {
56+
const schema = getSchema('text chat');
57+
58+
const props = is(schema.input_schema.properties);
59+
expect(props.message).toBeDefined();
60+
expect(is(props.message).type).toBe('array');
61+
});
62+
63+
it('name uses underscore-separated path', () => {
64+
const schema = getSchema('speech synthesize');
65+
66+
expect(schema.name).toBe('mmx_speech_synthesize');
67+
});
68+
});
69+
70+
describe('registry getAllCommands filtering', () => {
71+
it('returns all registered commands', () => {
72+
const commands = registry.getAllCommands();
73+
expect(commands.length).toBeGreaterThan(10);
74+
// Should contain real commands
75+
const names = commands.map(c => c.name);
76+
expect(names).toContain('text chat');
77+
expect(names).toContain('image generate');
78+
expect(names).toContain('speech synthesize');
79+
});
80+
81+
it('filters out auth, config, update prefixes', () => {
82+
const SKIP = ['auth ', 'config ', 'update'];
83+
const commands = registry.getAllCommands();
84+
const filtered = commands.filter(c => !SKIP.some(p => c.name.startsWith(p)));
85+
86+
const names = filtered.map(c => c.name);
87+
expect(names.every(n => !n.startsWith('auth '))).toBe(true);
88+
expect(names.every(n => !n.startsWith('config '))).toBe(true);
89+
// Real commands remain
90+
expect(names.some(n => n === 'text chat')).toBe(true);
91+
expect(names.some(n => n === 'image generate')).toBe(true);
92+
expect(names.some(n => n === 'music generate')).toBe(true);
93+
});
94+
95+
it('every filtered command generates valid schema', () => {
96+
const SKIP = ['auth ', 'config ', 'update'];
97+
const commands = registry.getAllCommands();
98+
const filtered = commands.filter(c => !SKIP.some(p => c.name.startsWith(p)));
99+
100+
for (const cmd of filtered) {
101+
const schema = generateToolSchema(cmd) as unknown as ToolSchema;
102+
expect(schema.name).toMatch(/^mmx_\w/);
103+
expect(schema.input_schema.type).toBe('object');
104+
expect(typeof schema.description).toBe('string');
105+
}
106+
});
107+
});

test/commands/music/cover.test.ts

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import { describe, it, expect } from 'bun:test';
2+
import { default as coverCommand } from '../../../src/commands/music/cover';
3+
4+
const baseConfig = {
5+
apiKey: 'test-key',
6+
region: 'global' as const,
7+
baseUrl: 'https://api.mmx.io',
8+
output: 'text' as const,
9+
timeout: 10,
10+
verbose: false,
11+
quiet: false,
12+
noColor: true,
13+
yes: false,
14+
dryRun: false,
15+
nonInteractive: true,
16+
async: false,
17+
};
18+
19+
const baseFlags = {
20+
quiet: false,
21+
verbose: false,
22+
noColor: true,
23+
yes: false,
24+
dryRun: false,
25+
help: false,
26+
nonInteractive: true,
27+
async: false,
28+
};
29+
30+
describe('music cover command', () => {
31+
it('has correct name', () => {
32+
expect(coverCommand.name).toBe('music cover');
33+
});
34+
35+
it('requires --prompt', async () => {
36+
await expect(
37+
coverCommand.execute(baseConfig, baseFlags),
38+
).rejects.toThrow('--prompt is required');
39+
});
40+
41+
it('requires either --audio or --audio-file', async () => {
42+
await expect(
43+
coverCommand.execute(
44+
baseConfig,
45+
{ ...baseFlags, prompt: 'Indie folk cover' },
46+
),
47+
).rejects.toThrow('One of --audio <url> or --audio-file <path> is required');
48+
});
49+
50+
it('rejects using both --audio and --audio-file', async () => {
51+
await expect(
52+
coverCommand.execute(
53+
baseConfig,
54+
{ ...baseFlags, prompt: 'Indie folk', audio: 'https://example.com/song.mp3', audioFile: '/tmp/ref.mp3' },
55+
),
56+
).rejects.toThrow('Use either --audio or --audio-file, not both');
57+
});
58+
59+
it('builds correct request body in dry-run with --audio', async () => {
60+
let captured = '';
61+
const origLog = console.log;
62+
console.log = (msg: string) => { captured += msg; };
63+
64+
try {
65+
await coverCommand.execute(
66+
{ ...baseConfig, dryRun: true, output: 'json' as const },
67+
{
68+
...baseFlags,
69+
dryRun: true,
70+
prompt: 'Jazz cover',
71+
audio: 'https://example.com/ref.mp3',
72+
},
73+
);
74+
} catch {
75+
// dry-run may resolve or reject
76+
}
77+
78+
console.log = origLog;
79+
const parsed = JSON.parse(captured);
80+
expect(parsed.request.model).toMatch(/^music-cover/);
81+
expect(parsed.request.prompt).toBe('Jazz cover');
82+
expect(parsed.request.audio_url).toBe('https://example.com/ref.mp3');
83+
expect(parsed.request.output_format).toBe('hex');
84+
});
85+
86+
it('accepts optional --lyrics', async () => {
87+
let captured = '';
88+
const origLog = console.log;
89+
console.log = (msg: string) => { captured += msg; };
90+
91+
try {
92+
await coverCommand.execute(
93+
{ ...baseConfig, dryRun: true, output: 'json' as const },
94+
{
95+
...baseFlags,
96+
dryRun: true,
97+
prompt: 'Pop cover',
98+
audio: 'https://example.com/ref.mp3',
99+
lyrics: 'New lyrics here',
100+
},
101+
);
102+
} catch {
103+
// dry-run may resolve or reject
104+
}
105+
106+
console.log = origLog;
107+
const parsed = JSON.parse(captured);
108+
expect(parsed.request.lyrics).toBe('New lyrics here');
109+
});
110+
111+
it('accepts optional --seed', async () => {
112+
let captured = '';
113+
const origLog = console.log;
114+
console.log = (msg: string) => { captured += msg; };
115+
116+
try {
117+
await coverCommand.execute(
118+
{ ...baseConfig, dryRun: true, output: 'json' as const },
119+
{
120+
...baseFlags,
121+
dryRun: true,
122+
prompt: 'Rock cover',
123+
audio: 'https://example.com/ref.mp3',
124+
seed: 42,
125+
},
126+
);
127+
} catch {
128+
// dry-run may resolve or reject
129+
}
130+
131+
console.log = origLog;
132+
const parsed = JSON.parse(captured);
133+
expect(parsed.request.seed).toBe(42);
134+
});
135+
136+
it('rejects invalid model', async () => {
137+
await expect(
138+
coverCommand.execute(
139+
{ ...baseConfig, dryRun: true },
140+
{
141+
...baseFlags,
142+
prompt: 'Folk cover',
143+
audio: 'https://example.com/ref.mp3',
144+
model: 'music-2.6',
145+
},
146+
),
147+
).rejects.toThrow('Invalid model');
148+
});
149+
150+
it('rejects invalid audio format', async () => {
151+
await expect(
152+
coverCommand.execute(
153+
{ ...baseConfig, dryRun: true },
154+
{
155+
...baseFlags,
156+
prompt: 'Folk cover',
157+
audio: 'https://example.com/ref.mp3',
158+
format: 'opus',
159+
},
160+
),
161+
).rejects.toThrow('Invalid audio format');
162+
});
163+
164+
it.each(['mp3', 'wav', 'pcm'])(
165+
'accepts %s format in dry-run',
166+
async (fmt) => {
167+
let captured = '';
168+
const origLog = console.log;
169+
console.log = (msg: string) => { captured += msg; };
170+
try {
171+
await coverCommand.execute(
172+
{ ...baseConfig, dryRun: true, output: 'json' as const },
173+
{
174+
...baseFlags,
175+
dryRun: true,
176+
prompt: 'Folk cover',
177+
audio: 'https://example.com/ref.mp3',
178+
format: fmt,
179+
},
180+
);
181+
const parsed = JSON.parse(captured);
182+
expect(parsed.request.audio_setting.format).toBe(fmt);
183+
} finally {
184+
console.log = origLog;
185+
}
186+
},
187+
);
188+
189+
it('has expected options', () => {
190+
const flags = coverCommand.options?.map(o => o.flag) ?? [];
191+
expect(flags.some(f => f.startsWith('--audio'))).toBe(true);
192+
expect(flags.some(f => f.startsWith('--audio-file'))).toBe(true);
193+
expect(flags.some(f => f.startsWith('--lyrics'))).toBe(true);
194+
expect(flags.some(f => f.startsWith('--seed'))).toBe(true);
195+
expect(flags.some(f => f.startsWith('--format'))).toBe(true);
196+
expect(flags.some(f => f.startsWith('--out'))).toBe(true);
197+
});
198+
199+
it('has examples with --audio and --audio-file usage', () => {
200+
const examples = coverCommand.examples ?? [];
201+
const joined = examples.join(' ');
202+
expect(joined).toContain('--audio');
203+
expect(joined).toContain('--audio-file');
204+
});
205+
});

0 commit comments

Comments
 (0)