- Updated Jest configuration to support ESM and improve test coverage - Added comprehensive test files for helpers, index, context, and HASS integration - Configured coverage reporting and added new test scripts - Updated Jest resolver to handle module resolution for chalk and related packages - Introduced new test setup files for mocking and environment configuration
286 lines
10 KiB
TypeScript
286 lines
10 KiB
TypeScript
import { jest, describe, beforeEach, afterEach, it, expect } from '@jest/globals';
|
|
import type { Mock } from 'jest-mock';
|
|
import { LiteMCP } from 'litemcp';
|
|
|
|
// Mock environment variables
|
|
process.env.HASS_HOST = 'http://localhost:8123';
|
|
process.env.HASS_TOKEN = 'test_token';
|
|
|
|
// Mock fetch
|
|
const mockFetch = jest.fn().mockImplementation(
|
|
async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {
|
|
return {} as Response;
|
|
}
|
|
) as unknown as jest.MockedFunction<typeof fetch>;
|
|
global.fetch = mockFetch;
|
|
|
|
// Mock LiteMCP
|
|
jest.mock('litemcp', () => {
|
|
return {
|
|
LiteMCP: jest.fn().mockImplementation(() => ({
|
|
addTool: jest.fn(),
|
|
start: jest.fn().mockResolvedValue(undefined)
|
|
}))
|
|
};
|
|
});
|
|
|
|
// Mock get_hass
|
|
jest.unstable_mockModule('../src/hass/index.js', () => ({
|
|
get_hass: jest.fn().mockResolvedValue({
|
|
services: {
|
|
light: {
|
|
turn_on: jest.fn(),
|
|
turn_off: jest.fn()
|
|
}
|
|
}
|
|
})
|
|
}));
|
|
|
|
interface Tool {
|
|
name: string;
|
|
execute: (...args: any[]) => Promise<any>;
|
|
}
|
|
|
|
describe('Home Assistant MCP Server', () => {
|
|
beforeEach(async () => {
|
|
// Reset all mocks
|
|
jest.clearAllMocks();
|
|
mockFetch.mockReset();
|
|
|
|
// Import the module which will execute the main function
|
|
await import('../src/index.js');
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.resetModules();
|
|
});
|
|
|
|
describe('list_devices tool', () => {
|
|
it('should successfully list devices', async () => {
|
|
// Mock the fetch response for listing devices
|
|
const mockDevices = [
|
|
{
|
|
entity_id: 'light.living_room',
|
|
state: 'on',
|
|
attributes: { brightness: 255 }
|
|
},
|
|
{
|
|
entity_id: 'climate.bedroom',
|
|
state: 'heat',
|
|
attributes: { temperature: 22 }
|
|
}
|
|
];
|
|
|
|
mockFetch.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => mockDevices
|
|
} as Response);
|
|
|
|
// Get the tool registration
|
|
const liteMcpInstance = (LiteMCP as jest.MockedClass<typeof LiteMCP>).mock.results[0].value;
|
|
const addToolCalls = liteMcpInstance.addTool.mock.calls;
|
|
const listDevicesTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'list_devices')?.[0] as Tool;
|
|
|
|
// Execute the tool
|
|
const result = await listDevicesTool.execute({});
|
|
|
|
// Verify the results
|
|
expect(result.success).toBe(true);
|
|
expect(result.devices).toEqual({
|
|
light: [{
|
|
entity_id: 'light.living_room',
|
|
state: 'on',
|
|
attributes: { brightness: 255 }
|
|
}],
|
|
climate: [{
|
|
entity_id: 'climate.bedroom',
|
|
state: 'heat',
|
|
attributes: { temperature: 22 }
|
|
}]
|
|
});
|
|
});
|
|
|
|
it('should handle fetch errors', async () => {
|
|
// Mock a fetch error
|
|
mockFetch.mockRejectedValueOnce(new Error('Network error'));
|
|
|
|
// Get the tool registration
|
|
const liteMcpInstance = (LiteMCP as jest.MockedClass<typeof LiteMCP>).mock.results[0].value;
|
|
const addToolCalls = liteMcpInstance.addTool.mock.calls;
|
|
const listDevicesTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'list_devices')?.[0] as Tool;
|
|
|
|
// Execute the tool
|
|
const result = await listDevicesTool.execute({});
|
|
|
|
// Verify error handling
|
|
expect(result.success).toBe(false);
|
|
expect(result.message).toBe('Network error');
|
|
});
|
|
});
|
|
|
|
describe('control tool', () => {
|
|
it('should successfully control a light device', async () => {
|
|
// Mock successful service call
|
|
mockFetch.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({})
|
|
} as Response);
|
|
|
|
// Get the tool registration
|
|
const liteMcpInstance = (LiteMCP as jest.MockedClass<typeof LiteMCP>).mock.results[0].value;
|
|
const addToolCalls = liteMcpInstance.addTool.mock.calls;
|
|
const controlTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'control')?.[0] as Tool;
|
|
|
|
// Execute the tool
|
|
const result = await controlTool.execute({
|
|
command: 'turn_on',
|
|
entity_id: 'light.living_room',
|
|
brightness: 255
|
|
});
|
|
|
|
// Verify the results
|
|
expect(result.success).toBe(true);
|
|
expect(result.message).toBe('Successfully executed turn_on for light.living_room');
|
|
|
|
// Verify the fetch call
|
|
expect(mockFetch).toHaveBeenCalledWith(
|
|
'http://localhost:8123/api/services/light/turn_on',
|
|
{
|
|
method: 'POST',
|
|
headers: {
|
|
Authorization: 'Bearer test_token',
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
entity_id: 'light.living_room',
|
|
brightness: 255
|
|
})
|
|
}
|
|
);
|
|
});
|
|
|
|
it('should handle unsupported domains', async () => {
|
|
// Get the tool registration
|
|
const liteMcpInstance = (LiteMCP as jest.MockedClass<typeof LiteMCP>).mock.results[0].value;
|
|
const addToolCalls = liteMcpInstance.addTool.mock.calls;
|
|
const controlTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'control')?.[0] as Tool;
|
|
|
|
// Execute the tool with an unsupported domain
|
|
const result = await controlTool.execute({
|
|
command: 'turn_on',
|
|
entity_id: 'unsupported.device'
|
|
});
|
|
|
|
// Verify error handling
|
|
expect(result.success).toBe(false);
|
|
expect(result.message).toBe('Unsupported domain: unsupported');
|
|
});
|
|
|
|
it('should handle service call errors', async () => {
|
|
// Mock a failed service call
|
|
mockFetch.mockResolvedValueOnce({
|
|
ok: false,
|
|
statusText: 'Service unavailable'
|
|
} as Response);
|
|
|
|
// Get the tool registration
|
|
const liteMcpInstance = (LiteMCP as jest.MockedClass<typeof LiteMCP>).mock.results[0].value;
|
|
const addToolCalls = liteMcpInstance.addTool.mock.calls;
|
|
const controlTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'control')?.[0] as Tool;
|
|
|
|
// Execute the tool
|
|
const result = await controlTool.execute({
|
|
command: 'turn_on',
|
|
entity_id: 'light.living_room'
|
|
});
|
|
|
|
// Verify error handling
|
|
expect(result.success).toBe(false);
|
|
expect(result.message).toContain('Failed to execute turn_on for light.living_room');
|
|
});
|
|
|
|
it('should handle climate device controls', async () => {
|
|
// Mock successful service call
|
|
mockFetch.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({})
|
|
} as Response);
|
|
|
|
// Get the tool registration
|
|
const liteMcpInstance = (LiteMCP as jest.MockedClass<typeof LiteMCP>).mock.results[0].value;
|
|
const addToolCalls = liteMcpInstance.addTool.mock.calls;
|
|
const controlTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'control')?.[0] as Tool;
|
|
|
|
// Execute the tool
|
|
const result = await controlTool.execute({
|
|
command: 'set_temperature',
|
|
entity_id: 'climate.bedroom',
|
|
temperature: 22,
|
|
target_temp_high: 24,
|
|
target_temp_low: 20
|
|
});
|
|
|
|
// Verify the results
|
|
expect(result.success).toBe(true);
|
|
expect(result.message).toBe('Successfully executed set_temperature for climate.bedroom');
|
|
|
|
// Verify the fetch call
|
|
expect(mockFetch).toHaveBeenCalledWith(
|
|
'http://localhost:8123/api/services/climate/set_temperature',
|
|
{
|
|
method: 'POST',
|
|
headers: {
|
|
Authorization: 'Bearer test_token',
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
entity_id: 'climate.bedroom',
|
|
temperature: 22,
|
|
target_temp_high: 24,
|
|
target_temp_low: 20
|
|
})
|
|
}
|
|
);
|
|
});
|
|
|
|
it('should handle cover device controls', async () => {
|
|
// Mock successful service call
|
|
mockFetch.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({})
|
|
} as Response);
|
|
|
|
// Get the tool registration
|
|
const liteMcpInstance = (LiteMCP as jest.MockedClass<typeof LiteMCP>).mock.results[0].value;
|
|
const addToolCalls = liteMcpInstance.addTool.mock.calls;
|
|
const controlTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'control')?.[0] as Tool;
|
|
|
|
// Execute the tool
|
|
const result = await controlTool.execute({
|
|
command: 'set_position',
|
|
entity_id: 'cover.living_room',
|
|
position: 50
|
|
});
|
|
|
|
// Verify the results
|
|
expect(result.success).toBe(true);
|
|
expect(result.message).toBe('Successfully executed set_position for cover.living_room');
|
|
|
|
// Verify the fetch call
|
|
expect(mockFetch).toHaveBeenCalledWith(
|
|
'http://localhost:8123/api/services/cover/set_position',
|
|
{
|
|
method: 'POST',
|
|
headers: {
|
|
Authorization: 'Bearer test_token',
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
entity_id: 'cover.living_room',
|
|
position: 50
|
|
})
|
|
}
|
|
);
|
|
});
|
|
});
|
|
});
|