/**
* Unit tests for separate_stems tool
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { separateStemsTool, handleSeparateStems } from '../../../src/tools/separate-stems.js';
// Mock the http module
vi.mock('../../../src/utils/http.js', () => ({
httpPost: vi.fn(),
buildApiUrl: vi.fn((endpoint) => `https://api.ircamamplify.io/v1${endpoint}`),
}));
// Mock auth
vi.mock('../../../src/utils/auth.js', () => ({
getApiKey: vi.fn(() => 'test-api-key'),
hasApiKey: vi.fn(() => true),
createAuthHeaders: vi.fn(() => ({
Authorization: 'Bearer test-api-key',
'Content-Type': 'application/json',
})),
API_KEY_ENV_VAR: 'IRCAM_AMPLIFY_API_KEY',
}));
import { httpPost } from '../../../src/utils/http.js';
describe('separate_stems tool', () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.resetAllMocks();
});
describe('tool definition', () => {
it('should have correct name', () => {
expect(separateStemsTool.name).toBe('separate_stems');
});
it('should have description mentioning stems', () => {
expect(separateStemsTool.description).toBeTruthy();
expect(separateStemsTool.description).toContain('vocals');
expect(separateStemsTool.description).toContain('drums');
expect(separateStemsTool.description).toContain('bass');
});
it('should require audio_url parameter', () => {
expect(separateStemsTool.inputSchema).toBeDefined();
expect(separateStemsTool.inputSchema.required).toContain('audio_url');
});
});
describe('handleSeparateStems', () => {
it('should return stem URLs for synchronous result', async () => {
const mockResponse = {
ok: true,
status: 200,
data: {
result: {
stems: {
vocals: { url: 'https://cdn.example.com/vocals.wav' },
drums: { url: 'https://cdn.example.com/drums.wav' },
bass: { url: 'https://cdn.example.com/bass.wav' },
other: { url: 'https://cdn.example.com/other.wav' },
},
},
},
};
vi.mocked(httpPost).mockResolvedValue(mockResponse);
const result = await handleSeparateStems({
audio_url: 'https://example.com/song.mp3',
});
expect(result).toEqual({
vocals_url: 'https://cdn.example.com/vocals.wav',
drums_url: 'https://cdn.example.com/drums.wav',
bass_url: 'https://cdn.example.com/bass.wav',
other_url: 'https://cdn.example.com/other.wav',
});
});
it('should return job_id for async processing', async () => {
const mockResponse = {
ok: true,
status: 200,
data: {
job_id: 'job-123-abc',
},
};
vi.mocked(httpPost).mockResolvedValue(mockResponse);
const result = await handleSeparateStems({
audio_url: 'https://example.com/long-song.mp3',
});
expect(result).toEqual({
job_id: 'job-123-abc',
});
});
it('should throw error for invalid URL', async () => {
await expect(
handleSeparateStems({ audio_url: '' })
).rejects.toMatchObject({
code: 'INVALID_URL',
});
});
it('should throw error when API returns error', async () => {
const mockError = {
ok: false,
status: 413,
error: {
code: 'FILE_TOO_LARGE',
message: 'File too large',
suggestion: 'Use a smaller file',
},
};
vi.mocked(httpPost).mockResolvedValue(mockError);
await expect(
handleSeparateStems({ audio_url: 'https://example.com/huge.mp3' })
).rejects.toMatchObject({
code: 'FILE_TOO_LARGE',
});
});
it('should call API with audio URL in body', async () => {
const mockResponse = {
ok: true,
status: 200,
data: {
job_id: 'job-456',
},
};
vi.mocked(httpPost).mockResolvedValue(mockResponse);
await handleSeparateStems({
audio_url: 'https://example.com/test.mp3',
});
// Verify httpPost was called with audio URL in body
expect(httpPost).toHaveBeenCalled();
const calls = vi.mocked(httpPost).mock.calls;
expect(calls[0][1]).toEqual({ url: 'https://example.com/test.mp3' });
});
});
});