Enhance Jest configuration and testing infrastructure

- 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
This commit is contained in:
jango-blockchained
2025-01-30 09:04:07 +01:00
parent f908d83cbf
commit d7e5fcf764
15 changed files with 1077 additions and 282 deletions

286
__tests__/index.test.ts Normal file
View File

@@ -0,0 +1,286 @@
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
})
}
);
});
});
});