Update project configuration and testing infrastructure

- Refactored Jest configuration for improved ESM and TypeScript support
- Updated `jest.setup.ts` with comprehensive test environment configuration
- Enhanced mocking for WebSocket, console, and external dependencies
- Adjusted package.json dependencies and scripts
- Updated tsconfig.json with decorator and test exclusion settings
- Improved test coverage configuration and reporting
- Simplified test file structure and mocking strategies
This commit is contained in:
jango-blockchained
2025-01-30 20:06:40 +01:00
parent e1e0a45acc
commit 96aaffd952
11 changed files with 2244 additions and 459 deletions

View File

@@ -1,14 +0,0 @@
{
"name": "MCP SSE Subscribe Flow",
"nodes": [
{
"id": "sse_subscribe",
"type": "http request",
"method": "GET",
"url": "http://localhost:3000/sse",
"ret": "txt",
"persist": true,
"name": "SSE Subscription"
}
]
}

View File

@@ -2,8 +2,7 @@ import { jest, describe, beforeEach, it, expect } from '@jest/globals';
import { z } from 'zod'; import { z } from 'zod';
import { DomainSchema } from '../../src/schemas.js'; import { DomainSchema } from '../../src/schemas.js';
type MockResponse = { success: true }; type MockResponse = { success: boolean };
type MockFn = jest.Mock<Promise<MockResponse>, any[]>;
// Define types for tool and server // Define types for tool and server
interface Tool { interface Tool {
@@ -14,16 +13,16 @@ interface Tool {
} }
interface MockService { interface MockService {
[key: string]: MockFn; [key: string]: jest.Mock<Promise<MockResponse>>;
} }
interface MockServices { interface MockServices {
light: { light: {
turn_on: MockFn; turn_on: jest.Mock<Promise<MockResponse>>;
turn_off: MockFn; turn_off: jest.Mock<Promise<MockResponse>>;
}; };
climate: { climate: {
set_temperature: MockFn; set_temperature: jest.Mock<Promise<MockResponse>>;
}; };
} }
@@ -46,10 +45,8 @@ class MockLiteMCP {
} }
} }
const createMockFn = () => { const createMockFn = (): jest.Mock<Promise<MockResponse>> => {
const fn = jest.fn(); return jest.fn<() => Promise<MockResponse>>().mockResolvedValue({ success: true });
fn.mockReturnValue(Promise.resolve({ success: true as const }));
return fn as unknown as MockFn;
}; };
// Mock the Home Assistant instance // Mock the Home Assistant instance
@@ -65,33 +62,26 @@ const mockHassServices: MockHassInstance = {
}, },
}; };
jest.mock('../../src/hass/index.js', () => ({ // Mock get_hass function
get_hass: jest.fn().mockReturnValue(Promise.resolve(mockHassServices)), const get_hass = jest.fn<() => Promise<MockHassInstance>>().mockResolvedValue(mockHassServices);
}));
describe('MCP Server Context and Tools', () => { describe('Context Tests', () => {
let server: MockLiteMCP; let mockTool: Tool;
beforeEach(async () => { beforeEach(() => {
server = new MockLiteMCP('home-assistant', '0.1.0'); mockTool = {
name: 'test_tool',
// Add the control tool to the server description: 'A test tool',
server.addTool({ execute: jest.fn<(params: any) => Promise<MockResponse>>().mockResolvedValue({ success: true }),
name: 'control', parameters: z.object({
description: 'Control Home Assistant devices', test: z.string()
parameters: DomainSchema, })
execute: createMockFn(), };
});
}); });
it('should initialize with correct name and version', () => { // Add your test cases here
expect(server.name).toBe('home-assistant'); it('should execute tool successfully', async () => {
expect(server.version).toBe('0.1.0'); const result = await mockTool.execute({ test: 'value' });
}); expect(result.success).toBe(true);
it('should add and retrieve tools', () => {
const tools = server.getTools();
expect(tools).toHaveLength(1);
expect(tools[0].name).toBe('control');
}); });
}); });

View File

@@ -1,5 +1,4 @@
import { jest, describe, beforeEach, afterEach, it, expect } from '@jest/globals'; import { jest, describe, beforeEach, afterEach, it, expect } from '@jest/globals';
import type { Mock } from 'jest-mock';
import { LiteMCP } from 'litemcp'; import { LiteMCP } from 'litemcp';
import { get_hass } from '../src/hass/index.js'; import { get_hass } from '../src/hass/index.js';
import type { WebSocket } from 'ws'; import type { WebSocket } from 'ws';
@@ -34,29 +33,10 @@ const mockFetchResponse = {
redirect: () => Promise.resolve(new Response()) redirect: () => Promise.resolve(new Response())
} as Response; } as Response;
const mockFetch = jest.fn(async (_input: string | URL | Request, _init?: RequestInit) => mockFetchResponse) as jest.MockedFunction<typeof fetch>; const mockFetch = jest.fn(async (_input: string | URL | Request, _init?: RequestInit) => mockFetchResponse);
(global as any).fetch = mockFetch; (global as any).fetch = mockFetch;
// Mock LiteMCP // Mock LiteMCP
jest.mock('litemcp', () => ({
LiteMCP: jest.fn().mockImplementation(() => ({
addTool: jest.fn(),
start: jest.fn()
}))
}));
// Mock get_hass
jest.unstable_mockModule('../src/hass/index.js', () => ({
get_hass: jest.fn().mockReturnValue({
services: {
light: {
turn_on: jest.fn(),
turn_off: jest.fn()
}
}
})
}));
interface Tool { interface Tool {
name: string; name: string;
description: string; description: string;
@@ -64,6 +44,52 @@ interface Tool {
execute: (params: Record<string, unknown>) => Promise<unknown>; execute: (params: Record<string, unknown>) => Promise<unknown>;
} }
type MockFunction<T = any> = jest.Mock<Promise<T>, any[]>;
interface MockLiteMCPInstance {
addTool: ReturnType<typeof jest.fn>;
start: ReturnType<typeof jest.fn>;
}
const mockLiteMCPInstance: MockLiteMCPInstance = {
addTool: jest.fn(),
start: jest.fn().mockResolvedValue(undefined)
};
jest.mock('litemcp', () => ({
LiteMCP: jest.fn(() => mockLiteMCPInstance)
}));
// Mock get_hass
interface MockServices {
light: {
turn_on: jest.Mock;
turn_off: jest.Mock;
};
climate: {
set_temperature: jest.Mock;
};
}
interface MockHassInstance {
services: MockServices;
}
// Create mock services
const mockServices: MockServices = {
light: {
turn_on: jest.fn().mockResolvedValue({ success: true }),
turn_off: jest.fn().mockResolvedValue({ success: true })
},
climate: {
set_temperature: jest.fn().mockResolvedValue({ success: true })
}
};
jest.unstable_mockModule('../src/hass/index.js', () => ({
get_hass: jest.fn().mockResolvedValue({ services: mockServices })
}));
interface TestResponse { interface TestResponse {
success: boolean; success: boolean;
message?: string; message?: string;
@@ -88,10 +114,10 @@ type WebSocketEventListener = (event: Event) => void;
type WebSocketMessageListener = (event: MessageEvent) => void; type WebSocketMessageListener = (event: MessageEvent) => void;
interface MockWebSocketInstance { interface MockWebSocketInstance {
addEventListener: jest.MockedFunction<typeof Function>; addEventListener: jest.Mock;
removeEventListener: jest.MockedFunction<typeof Function>; removeEventListener: jest.Mock;
send: jest.MockedFunction<typeof Function>; send: jest.Mock;
close: jest.MockedFunction<typeof Function>; close: jest.Mock;
readyState: number; readyState: number;
binaryType: 'blob' | 'arraybuffer'; binaryType: 'blob' | 'arraybuffer';
bufferedAmount: number; bufferedAmount: number;
@@ -109,10 +135,10 @@ interface MockWebSocketInstance {
} }
const createMockWebSocket = (): MockWebSocketInstance => ({ const createMockWebSocket = (): MockWebSocketInstance => ({
addEventListener: jest.fn() as jest.MockedFunction<typeof Function>, addEventListener: jest.fn(),
removeEventListener: jest.fn() as jest.MockedFunction<typeof Function>, removeEventListener: jest.fn(),
send: jest.fn() as jest.MockedFunction<typeof Function>, send: jest.fn(),
close: jest.fn() as jest.MockedFunction<typeof Function>, close: jest.fn(),
readyState: 0, readyState: 0,
binaryType: 'blob', binaryType: 'blob',
bufferedAmount: 0, bufferedAmount: 0,
@@ -130,7 +156,15 @@ const createMockWebSocket = (): MockWebSocketInstance => ({
}); });
describe('Home Assistant MCP Server', () => { describe('Home Assistant MCP Server', () => {
let mockHass: MockHassInstance;
let liteMcpInstance: MockLiteMCPInstance;
let addToolCalls: Array<[Tool]>;
beforeEach(async () => { beforeEach(async () => {
mockHass = {
services: mockServices
};
// Reset all mocks // Reset all mocks
jest.clearAllMocks(); jest.clearAllMocks();
mockFetch.mockClear(); mockFetch.mockClear();
@@ -141,12 +175,29 @@ describe('Home Assistant MCP Server', () => {
// Mock WebSocket // Mock WebSocket
const mockWs = createMockWebSocket(); const mockWs = createMockWebSocket();
(global as any).WebSocket = jest.fn(() => mockWs); (global as any).WebSocket = jest.fn(() => mockWs);
// Get the mock instance
liteMcpInstance = mockLiteMCPInstance;
addToolCalls = liteMcpInstance.addTool.mock.calls as Array<[Tool]>;
}); });
afterEach(() => { afterEach(() => {
jest.resetModules(); jest.resetModules();
}); });
it('should connect to Home Assistant', async () => {
const hass = await get_hass();
expect(hass).toBeDefined();
expect(hass.services).toBeDefined();
expect(typeof hass.services.light.turn_on).toBe('function');
});
it('should reuse the same instance on subsequent calls', async () => {
const firstInstance = await get_hass();
const secondInstance = await get_hass();
expect(firstInstance).toBe(secondInstance);
});
describe('list_devices tool', () => { describe('list_devices tool', () => {
it('should successfully list devices', async () => { it('should successfully list devices', async () => {
// Mock the fetch response for listing devices // Mock the fetch response for listing devices
@@ -169,9 +220,12 @@ describe('Home Assistant MCP Server', () => {
} as Response); } as Response);
// Get the tool registration // Get the tool registration
const liteMcpInstance = (LiteMCP as jest.MockedClass<typeof LiteMCP>).mock.results[0].value; const listDevicesTool = addToolCalls.find(call => call[0].name === 'list_devices')?.[0];
const addToolCalls = liteMcpInstance.addTool.mock.calls; expect(listDevicesTool).toBeDefined();
const listDevicesTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'list_devices')?.[0] as Tool;
if (!listDevicesTool) {
throw new Error('list_devices tool not found');
}
// Execute the tool // Execute the tool
const result = (await listDevicesTool.execute({})) as TestResponse; const result = (await listDevicesTool.execute({})) as TestResponse;
@@ -197,9 +251,12 @@ describe('Home Assistant MCP Server', () => {
mockFetch.mockRejectedValueOnce(new Error('Network error')); mockFetch.mockRejectedValueOnce(new Error('Network error'));
// Get the tool registration // Get the tool registration
const liteMcpInstance = (LiteMCP as jest.MockedClass<typeof LiteMCP>).mock.results[0].value; const listDevicesTool = addToolCalls.find(call => call[0].name === 'list_devices')?.[0];
const addToolCalls = liteMcpInstance.addTool.mock.calls; expect(listDevicesTool).toBeDefined();
const listDevicesTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'list_devices')?.[0] as Tool;
if (!listDevicesTool) {
throw new Error('list_devices tool not found');
}
// Execute the tool // Execute the tool
const result = (await listDevicesTool.execute({})) as TestResponse; const result = (await listDevicesTool.execute({})) as TestResponse;
@@ -219,9 +276,12 @@ describe('Home Assistant MCP Server', () => {
} as Response); } as Response);
// Get the tool registration // Get the tool registration
const liteMcpInstance = (LiteMCP as jest.MockedClass<typeof LiteMCP>).mock.results[0].value; const controlTool = addToolCalls.find(call => call[0].name === 'control')?.[0];
const addToolCalls = liteMcpInstance.addTool.mock.calls; expect(controlTool).toBeDefined();
const controlTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'control')?.[0] as Tool;
if (!controlTool) {
throw new Error('control tool not found');
}
// Execute the tool // Execute the tool
const result = (await controlTool.execute({ const result = (await controlTool.execute({
@@ -253,9 +313,12 @@ describe('Home Assistant MCP Server', () => {
it('should handle unsupported domains', async () => { it('should handle unsupported domains', async () => {
// Get the tool registration // Get the tool registration
const liteMcpInstance = (LiteMCP as jest.MockedClass<typeof LiteMCP>).mock.results[0].value; const controlTool = addToolCalls.find(call => call[0].name === 'control')?.[0];
const addToolCalls = liteMcpInstance.addTool.mock.calls; expect(controlTool).toBeDefined();
const controlTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'control')?.[0] as Tool;
if (!controlTool) {
throw new Error('control tool not found');
}
// Execute the tool with an unsupported domain // Execute the tool with an unsupported domain
const result = (await controlTool.execute({ const result = (await controlTool.execute({
@@ -276,9 +339,12 @@ describe('Home Assistant MCP Server', () => {
} as Response); } as Response);
// Get the tool registration // Get the tool registration
const liteMcpInstance = (LiteMCP as jest.MockedClass<typeof LiteMCP>).mock.results[0].value; const controlTool = addToolCalls.find(call => call[0].name === 'control')?.[0];
const addToolCalls = liteMcpInstance.addTool.mock.calls; expect(controlTool).toBeDefined();
const controlTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'control')?.[0] as Tool;
if (!controlTool) {
throw new Error('control tool not found');
}
// Execute the tool // Execute the tool
const result = (await controlTool.execute({ const result = (await controlTool.execute({
@@ -299,9 +365,12 @@ describe('Home Assistant MCP Server', () => {
} as Response); } as Response);
// Get the tool registration // Get the tool registration
const liteMcpInstance = (LiteMCP as jest.MockedClass<typeof LiteMCP>).mock.results[0].value; const controlTool = addToolCalls.find(call => call[0].name === 'control')?.[0];
const addToolCalls = liteMcpInstance.addTool.mock.calls; expect(controlTool).toBeDefined();
const controlTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'control')?.[0] as Tool;
if (!controlTool) {
throw new Error('control tool not found');
}
// Execute the tool // Execute the tool
const result = (await controlTool.execute({ const result = (await controlTool.execute({
@@ -343,9 +412,12 @@ describe('Home Assistant MCP Server', () => {
} as Response); } as Response);
// Get the tool registration // Get the tool registration
const liteMcpInstance = (LiteMCP as jest.MockedClass<typeof LiteMCP>).mock.results[0].value; const controlTool = addToolCalls.find(call => call[0].name === 'control')?.[0];
const addToolCalls = liteMcpInstance.addTool.mock.calls; expect(controlTool).toBeDefined();
const controlTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'control')?.[0] as Tool;
if (!controlTool) {
throw new Error('control tool not found');
}
// Execute the tool // Execute the tool
const result = (await controlTool.execute({ const result = (await controlTool.execute({
@@ -399,9 +471,12 @@ describe('Home Assistant MCP Server', () => {
} as Response); } as Response);
// Get the tool registration // Get the tool registration
const liteMcpInstance = (LiteMCP as jest.MockedClass<typeof LiteMCP>).mock.results[0].value; const historyTool = addToolCalls.find(call => call[0].name === 'get_history')?.[0];
const addToolCalls = liteMcpInstance.addTool.mock.calls; expect(historyTool).toBeDefined();
const historyTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'get_history')?.[0] as Tool;
if (!historyTool) {
throw new Error('get_history tool not found');
}
// Execute the tool // Execute the tool
const result = (await historyTool.execute({ const result = (await historyTool.execute({
@@ -438,9 +513,12 @@ describe('Home Assistant MCP Server', () => {
it('should handle fetch errors', async () => { it('should handle fetch errors', async () => {
mockFetch.mockRejectedValueOnce(new Error('Network error')); mockFetch.mockRejectedValueOnce(new Error('Network error'));
const liteMcpInstance = (LiteMCP as jest.MockedClass<typeof LiteMCP>).mock.results[0].value; const historyTool = addToolCalls.find(call => call[0].name === 'get_history')?.[0];
const addToolCalls = liteMcpInstance.addTool.mock.calls; expect(historyTool).toBeDefined();
const historyTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'get_history')?.[0] as Tool;
if (!historyTool) {
throw new Error('get_history tool not found');
}
const result = (await historyTool.execute({ const result = (await historyTool.execute({
entity_id: 'light.living_room' entity_id: 'light.living_room'
@@ -477,9 +555,12 @@ describe('Home Assistant MCP Server', () => {
json: async () => mockScenes json: async () => mockScenes
} as Response); } as Response);
const liteMcpInstance = (LiteMCP as jest.MockedClass<typeof LiteMCP>).mock.results[0].value; const sceneTool = addToolCalls.find(call => call[0].name === 'scene')?.[0];
const addToolCalls = liteMcpInstance.addTool.mock.calls; expect(sceneTool).toBeDefined();
const sceneTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'scene')?.[0] as Tool;
if (!sceneTool) {
throw new Error('scene tool not found');
}
const result = (await sceneTool.execute({ const result = (await sceneTool.execute({
action: 'list' action: 'list'
@@ -506,9 +587,12 @@ describe('Home Assistant MCP Server', () => {
json: async () => ({}) json: async () => ({})
} as Response); } as Response);
const liteMcpInstance = (LiteMCP as jest.MockedClass<typeof LiteMCP>).mock.results[0].value; const sceneTool = addToolCalls.find(call => call[0].name === 'scene')?.[0];
const addToolCalls = liteMcpInstance.addTool.mock.calls; expect(sceneTool).toBeDefined();
const sceneTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'scene')?.[0] as Tool;
if (!sceneTool) {
throw new Error('scene tool not found');
}
const result = (await sceneTool.execute({ const result = (await sceneTool.execute({
action: 'activate', action: 'activate',
@@ -541,9 +625,12 @@ describe('Home Assistant MCP Server', () => {
json: async () => ({}) json: async () => ({})
} as Response); } as Response);
const liteMcpInstance = (LiteMCP as jest.MockedClass<typeof LiteMCP>).mock.results[0].value; const notifyTool = addToolCalls.find(call => call[0].name === 'notify')?.[0];
const addToolCalls = liteMcpInstance.addTool.mock.calls; expect(notifyTool).toBeDefined();
const notifyTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'notify')?.[0] as Tool;
if (!notifyTool) {
throw new Error('notify tool not found');
}
const result = (await notifyTool.execute({ const result = (await notifyTool.execute({
message: 'Test notification', message: 'Test notification',
@@ -578,9 +665,12 @@ describe('Home Assistant MCP Server', () => {
json: async () => ({}) json: async () => ({})
} as Response); } as Response);
const liteMcpInstance = (LiteMCP as jest.MockedClass<typeof LiteMCP>).mock.results[0].value; const notifyTool = addToolCalls.find(call => call[0].name === 'notify')?.[0];
const addToolCalls = liteMcpInstance.addTool.mock.calls; expect(notifyTool).toBeDefined();
const notifyTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'notify')?.[0] as Tool;
if (!notifyTool) {
throw new Error('notify tool not found');
}
await notifyTool.execute({ await notifyTool.execute({
message: 'Test notification' message: 'Test notification'
@@ -619,9 +709,12 @@ describe('Home Assistant MCP Server', () => {
json: async () => mockAutomations json: async () => mockAutomations
} as Response); } as Response);
const liteMcpInstance = (LiteMCP as jest.MockedClass<typeof LiteMCP>).mock.results[0].value; const automationTool = addToolCalls.find(call => call[0].name === 'automation')?.[0];
const addToolCalls = liteMcpInstance.addTool.mock.calls; expect(automationTool).toBeDefined();
const automationTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'automation')?.[0] as Tool;
if (!automationTool) {
throw new Error('automation tool not found');
}
const result = (await automationTool.execute({ const result = (await automationTool.execute({
action: 'list' action: 'list'
@@ -650,9 +743,12 @@ describe('Home Assistant MCP Server', () => {
json: async () => ({}) json: async () => ({})
} as Response); } as Response);
const liteMcpInstance = (LiteMCP as jest.MockedClass<typeof LiteMCP>).mock.results[0].value; const automationTool = addToolCalls.find(call => call[0].name === 'automation')?.[0];
const addToolCalls = liteMcpInstance.addTool.mock.calls; expect(automationTool).toBeDefined();
const automationTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'automation')?.[0] as Tool;
if (!automationTool) {
throw new Error('automation tool not found');
}
const result = (await automationTool.execute({ const result = (await automationTool.execute({
action: 'toggle', action: 'toggle',
@@ -683,9 +779,12 @@ describe('Home Assistant MCP Server', () => {
json: async () => ({}) json: async () => ({})
} as Response); } as Response);
const liteMcpInstance = (LiteMCP as jest.MockedClass<typeof LiteMCP>).mock.results[0].value; const automationTool = addToolCalls.find(call => call[0].name === 'automation')?.[0];
const addToolCalls = liteMcpInstance.addTool.mock.calls; expect(automationTool).toBeDefined();
const automationTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'automation')?.[0] as Tool;
if (!automationTool) {
throw new Error('automation tool not found');
}
const result = (await automationTool.execute({ const result = (await automationTool.execute({
action: 'trigger', action: 'trigger',
@@ -711,9 +810,12 @@ describe('Home Assistant MCP Server', () => {
}); });
it('should require automation_id for toggle and trigger actions', async () => { it('should require automation_id for toggle and trigger actions', async () => {
const liteMcpInstance = (LiteMCP as jest.MockedClass<typeof LiteMCP>).mock.results[0].value; const automationTool = addToolCalls.find(call => call[0].name === 'automation')?.[0];
const addToolCalls = liteMcpInstance.addTool.mock.calls; expect(automationTool).toBeDefined();
const automationTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'automation')?.[0] as Tool;
if (!automationTool) {
throw new Error('automation tool not found');
}
const result = (await automationTool.execute({ const result = (await automationTool.execute({
action: 'toggle' action: 'toggle'
@@ -756,9 +858,12 @@ describe('Home Assistant MCP Server', () => {
json: async () => mockAddons json: async () => mockAddons
} as Response); } as Response);
const liteMcpInstance = (LiteMCP as jest.MockedClass<typeof LiteMCP>).mock.results[0].value; const addonTool = addToolCalls.find(call => call[0].name === 'addon')?.[0];
const addToolCalls = liteMcpInstance.addTool.mock.calls; expect(addonTool).toBeDefined();
const addonTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'addon')?.[0] as Tool;
if (!addonTool) {
throw new Error('addon tool not found');
}
const result = (await addonTool.execute({ const result = (await addonTool.execute({
action: 'list' action: 'list'
@@ -774,9 +879,12 @@ describe('Home Assistant MCP Server', () => {
json: async () => ({ data: { state: 'installing' } }) json: async () => ({ data: { state: 'installing' } })
} as Response); } as Response);
const liteMcpInstance = (LiteMCP as jest.MockedClass<typeof LiteMCP>).mock.results[0].value; const addonTool = addToolCalls.find(call => call[0].name === 'addon')?.[0];
const addToolCalls = liteMcpInstance.addTool.mock.calls; expect(addonTool).toBeDefined();
const addonTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'addon')?.[0] as Tool;
if (!addonTool) {
throw new Error('addon tool not found');
}
const result = (await addonTool.execute({ const result = (await addonTool.execute({
action: 'install', action: 'install',
@@ -823,9 +931,12 @@ describe('Home Assistant MCP Server', () => {
json: async () => mockPackages json: async () => mockPackages
} as Response); } as Response);
const liteMcpInstance = (LiteMCP as jest.MockedClass<typeof LiteMCP>).mock.results[0].value; const packageTool = addToolCalls.find(call => call[0].name === 'package')?.[0];
const addToolCalls = liteMcpInstance.addTool.mock.calls; expect(packageTool).toBeDefined();
const packageTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'package')?.[0] as Tool;
if (!packageTool) {
throw new Error('package tool not found');
}
const result = (await packageTool.execute({ const result = (await packageTool.execute({
action: 'list', action: 'list',
@@ -842,9 +953,12 @@ describe('Home Assistant MCP Server', () => {
json: async () => ({}) json: async () => ({})
} as Response); } as Response);
const liteMcpInstance = (LiteMCP as jest.MockedClass<typeof LiteMCP>).mock.results[0].value; const packageTool = addToolCalls.find(call => call[0].name === 'package')?.[0];
const addToolCalls = liteMcpInstance.addTool.mock.calls; expect(packageTool).toBeDefined();
const packageTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'package')?.[0] as Tool;
if (!packageTool) {
throw new Error('package tool not found');
}
const result = (await packageTool.execute({ const result = (await packageTool.execute({
action: 'install', action: 'install',
@@ -902,9 +1016,12 @@ describe('Home Assistant MCP Server', () => {
json: async () => ({ automation_id: 'new_automation_1' }) json: async () => ({ automation_id: 'new_automation_1' })
} as Response); } as Response);
const liteMcpInstance = (LiteMCP as jest.MockedClass<typeof LiteMCP>).mock.results[0].value; const automationConfigTool = addToolCalls.find(call => call[0].name === 'automation_config')?.[0];
const addToolCalls = liteMcpInstance.addTool.mock.calls; expect(automationConfigTool).toBeDefined();
const automationConfigTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'automation_config')?.[0] as Tool;
if (!automationConfigTool) {
throw new Error('automation_config tool not found');
}
const result = (await automationConfigTool.execute({ const result = (await automationConfigTool.execute({
action: 'create', action: 'create',
@@ -941,9 +1058,12 @@ describe('Home Assistant MCP Server', () => {
json: async () => ({ automation_id: 'new_automation_2' }) json: async () => ({ automation_id: 'new_automation_2' })
} as Response); } as Response);
const liteMcpInstance = (LiteMCP as jest.MockedClass<typeof LiteMCP>).mock.results[0].value; const automationConfigTool = addToolCalls.find(call => call[0].name === 'automation_config')?.[0];
const addToolCalls = liteMcpInstance.addTool.mock.calls; expect(automationConfigTool).toBeDefined();
const automationConfigTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'automation_config')?.[0] as Tool;
if (!automationConfigTool) {
throw new Error('automation_config tool not found');
}
const result = (await automationConfigTool.execute({ const result = (await automationConfigTool.execute({
action: 'duplicate', action: 'duplicate',
@@ -975,9 +1095,12 @@ describe('Home Assistant MCP Server', () => {
}); });
it('should require config for create action', async () => { it('should require config for create action', async () => {
const liteMcpInstance = (LiteMCP as jest.MockedClass<typeof LiteMCP>).mock.results[0].value; const automationConfigTool = addToolCalls.find(call => call[0].name === 'automation_config')?.[0];
const addToolCalls = liteMcpInstance.addTool.mock.calls; expect(automationConfigTool).toBeDefined();
const automationConfigTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'automation_config')?.[0] as Tool;
if (!automationConfigTool) {
throw new Error('automation_config tool not found');
}
const result = (await automationConfigTool.execute({ const result = (await automationConfigTool.execute({
action: 'create' action: 'create'
@@ -988,9 +1111,12 @@ describe('Home Assistant MCP Server', () => {
}); });
it('should require automation_id for update action', async () => { it('should require automation_id for update action', async () => {
const liteMcpInstance = (LiteMCP as jest.MockedClass<typeof LiteMCP>).mock.results[0].value; const automationConfigTool = addToolCalls.find(call => call[0].name === 'automation_config')?.[0];
const addToolCalls = liteMcpInstance.addTool.mock.calls; expect(automationConfigTool).toBeDefined();
const automationConfigTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'automation_config')?.[0] as Tool;
if (!automationConfigTool) {
throw new Error('automation_config tool not found');
}
const result = (await automationConfigTool.execute({ const result = (await automationConfigTool.execute({
action: 'update', action: 'update',

View File

@@ -23,30 +23,30 @@
<div class='clearfix'> <div class='clearfix'>
<div class='fl pad1y space-right2'> <div class='fl pad1y space-right2'>
<span class="strong">37.98% </span> <span class="strong">22.12% </span>
<span class="quiet">Statements</span> <span class="quiet">Statements</span>
<span class='fraction'>128/337</span> <span class='fraction'>260/1175</span>
</div> </div>
<div class='fl pad1y space-right2'> <div class='fl pad1y space-right2'>
<span class="strong">33.06% </span> <span class="strong">19.23% </span>
<span class="quiet">Branches</span> <span class="quiet">Branches</span>
<span class='fraction'>41/124</span> <span class='fraction'>85/442</span>
</div> </div>
<div class='fl pad1y space-right2'> <div class='fl pad1y space-right2'>
<span class="strong">37.5% </span> <span class="strong">25.22% </span>
<span class="quiet">Functions</span> <span class="quiet">Functions</span>
<span class='fraction'>33/88</span> <span class='fraction'>57/226</span>
</div> </div>
<div class='fl pad1y space-right2'> <div class='fl pad1y space-right2'>
<span class="strong">38.34% </span> <span class="strong">22.2% </span>
<span class="quiet">Lines</span> <span class="quiet">Lines</span>
<span class='fraction'>125/326</span> <span class='fraction'>254/1144</span>
</div> </div>
@@ -79,18 +79,78 @@
</tr> </tr>
</thead> </thead>
<tbody><tr> <tbody><tr>
<td class="file high" data-value="src"><a href="src/index.html">src</a></td> <td class="file low" data-value="src"><a href="src/index.html">src</a></td>
<td data-value="100" class="pic high"> <td data-value="11.14" class="pic low">
<div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div> <div class="chart"><div class="cover-fill" style="width: 11%"></div><div class="cover-empty" style="width: 89%"></div></div>
</td> </td>
<td data-value="100" class="pct high">100%</td> <td data-value="11.14" class="pct low">11.14%</td>
<td data-value="33" class="abs high">33/33</td> <td data-value="296" class="abs low">33/296</td>
<td data-value="100" class="pct high">100%</td> <td data-value="0.71" class="pct low">0.71%</td>
<td data-value="1" class="abs high">1/1</td> <td data-value="139" class="abs low">1/139</td>
<td data-value="100" class="pct high">100%</td> <td data-value="5" class="pct low">5%</td>
<td data-value="1" class="abs high">1/1</td> <td data-value="20" class="abs low">1/20</td>
<td data-value="100" class="pct high">100%</td> <td data-value="11.22" class="pct low">11.22%</td>
<td data-value="33" class="abs high">33/33</td> <td data-value="294" class="abs low">33/294</td>
</tr>
<tr>
<td class="file low" data-value="src/ai/endpoints"><a href="src/ai/endpoints/index.html">src/ai/endpoints</a></td>
<td data-value="0" class="pic low">
<div class="chart"><div class="cover-fill" style="width: 0%"></div><div class="cover-empty" style="width: 100%"></div></div>
</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="39" class="abs low">0/39</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="7" class="abs low">0/7</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="8" class="abs low">0/8</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="38" class="abs low">0/38</td>
</tr>
<tr>
<td class="file low" data-value="src/ai/nlp"><a href="src/ai/nlp/index.html">src/ai/nlp</a></td>
<td data-value="25.37" class="pic low">
<div class="chart"><div class="cover-fill" style="width: 25%"></div><div class="cover-empty" style="width: 75%"></div></div>
</td>
<td data-value="25.37" class="pct low">25.37%</td>
<td data-value="134" class="abs low">34/134</td>
<td data-value="23.8" class="pct low">23.8%</td>
<td data-value="63" class="abs low">15/63</td>
<td data-value="16.66" class="pct low">16.66%</td>
<td data-value="30" class="abs low">5/30</td>
<td data-value="25.95" class="pct low">25.95%</td>
<td data-value="131" class="abs low">34/131</td>
</tr>
<tr>
<td class="file low" data-value="src/ai/templates"><a href="src/ai/templates/index.html">src/ai/templates</a></td>
<td data-value="0" class="pic low">
<div class="chart"><div class="cover-fill" style="width: 0%"></div><div class="cover-empty" style="width: 100%"></div></div>
</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="15" class="abs low">0/15</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="2" class="abs low">0/2</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="8" class="abs low">0/8</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="15" class="abs low">0/15</td>
</tr>
<tr>
<td class="file low" data-value="src/ai/types"><a href="src/ai/types/index.html">src/ai/types</a></td>
<td data-value="0" class="pic low">
<div class="chart"><div class="cover-fill" style="width: 0%"></div><div class="cover-empty" style="width: 100%"></div></div>
</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="10" class="abs low">0/10</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="2" class="abs low">0/2</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="1" class="abs low">0/1</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="10" class="abs low">0/10</td>
</tr> </tr>
<tr> <tr>
@@ -124,48 +184,108 @@
</tr> </tr>
<tr> <tr>
<td class="file high" data-value="src/hass"><a href="src/hass/index.html">src/hass</a></td> <td class="file low" data-value="src/hass"><a href="src/hass/index.html">src/hass</a></td>
<td data-value="87.5" class="pic high"> <td data-value="23.42" class="pic low">
<div class="chart"><div class="cover-fill" style="width: 87%"></div><div class="cover-empty" style="width: 13%"></div></div> <div class="chart"><div class="cover-fill" style="width: 23%"></div><div class="cover-empty" style="width: 77%"></div></div>
</td> </td>
<td data-value="87.5" class="pct high">87.5%</td> <td data-value="23.42" class="pct low">23.42%</td>
<td data-value="8" class="abs high">7/8</td> <td data-value="111" class="abs low">26/111</td>
<td data-value="40" class="pct low">40%</td> <td data-value="19.51" class="pct low">19.51%</td>
<td data-value="5" class="abs low">2/5</td> <td data-value="41" class="abs low">8/41</td>
<td data-value="50" class="pct medium">50%</td> <td data-value="13.04" class="pct low">13.04%</td>
<td data-value="2" class="abs medium">1/2</td> <td data-value="23" class="abs low">3/23</td>
<td data-value="87.5" class="pct high">87.5%</td> <td data-value="23.42" class="pct low">23.42%</td>
<td data-value="8" class="abs high">7/8</td> <td data-value="111" class="abs low">26/111</td>
</tr> </tr>
<tr> <tr>
<td class="file low" data-value="src/performance"><a href="src/performance/index.html">src/performance</a></td> <td class="file low" data-value="src/performance"><a href="src/performance/index.html">src/performance</a></td>
<td data-value="0" class="pic low"> <td data-value="26.86" class="pic low">
<div class="chart"><div class="cover-fill" style="width: 0%"></div><div class="cover-empty" style="width: 100%"></div></div> <div class="chart"><div class="cover-fill" style="width: 26%"></div><div class="cover-empty" style="width: 74%"></div></div>
</td> </td>
<td data-value="0" class="pct low">0%</td> <td data-value="26.86" class="pct low">26.86%</td>
<td data-value="67" class="abs low">0/67</td> <td data-value="67" class="abs low">18/67</td>
<td data-value="0" class="pct low">0%</td> <td data-value="63.63" class="pct medium">63.63%</td>
<td data-value="22" class="abs low">0/22</td> <td data-value="22" class="abs medium">14/22</td>
<td data-value="0" class="pct low">0%</td> <td data-value="33.33" class="pct low">33.33%</td>
<td data-value="21" class="abs low">0/21</td> <td data-value="21" class="abs low">7/21</td>
<td data-value="0" class="pct low">0%</td> <td data-value="28.33" class="pct low">28.33%</td>
<td data-value="60" class="abs low">0/60</td> <td data-value="60" class="abs low">17/60</td>
</tr> </tr>
<tr> <tr>
<td class="file low" data-value="src/security"><a href="src/security/index.html">src/security</a></td> <td class="file low" data-value="src/platforms/macos"><a href="src/platforms/macos/index.html">src/platforms/macos</a></td>
<td data-value="0" class="pic low"> <td data-value="0" class="pic low">
<div class="chart"><div class="cover-fill" style="width: 0%"></div><div class="cover-empty" style="width: 100%"></div></div> <div class="chart"><div class="cover-fill" style="width: 0%"></div><div class="cover-empty" style="width: 100%"></div></div>
</td> </td>
<td data-value="0" class="pct low">0%</td> <td data-value="0" class="pct low">0%</td>
<td data-value="57" class="abs low">0/57</td> <td data-value="82" class="abs low">0/82</td>
<td data-value="0" class="pct low">0%</td> <td data-value="0" class="pct low">0%</td>
<td data-value="22" class="abs low">0/22</td> <td data-value="25" class="abs low">0/25</td>
<td data-value="0" class="pct low">0%</td> <td data-value="0" class="pct low">0%</td>
<td data-value="7" class="abs low">0/7</td> <td data-value="13" class="abs low">0/13</td>
<td data-value="0" class="pct low">0%</td> <td data-value="0" class="pct low">0%</td>
<td data-value="56" class="abs low">0/56</td> <td data-value="79" class="abs low">0/79</td>
</tr>
<tr>
<td class="file low" data-value="src/schemas"><a href="src/schemas/index.html">src/schemas</a></td>
<td data-value="0" class="pic low">
<div class="chart"><div class="cover-fill" style="width: 0%"></div><div class="cover-empty" style="width: 100%"></div></div>
</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="8" class="abs low">0/8</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="0" class="abs high">0/0</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="0" class="abs high">0/0</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="8" class="abs low">0/8</td>
</tr>
<tr>
<td class="file medium" data-value="src/security"><a href="src/security/index.html">src/security</a></td>
<td data-value="75.43" class="pic medium">
<div class="chart"><div class="cover-fill" style="width: 75%"></div><div class="cover-empty" style="width: 25%"></div></div>
</td>
<td data-value="75.43" class="pct medium">75.43%</td>
<td data-value="57" class="abs medium">43/57</td>
<td data-value="25" class="pct low">25%</td>
<td data-value="20" class="abs low">5/20</td>
<td data-value="57.14" class="pct medium">57.14%</td>
<td data-value="7" class="abs medium">4/7</td>
<td data-value="75" class="pct medium">75%</td>
<td data-value="56" class="abs medium">42/56</td>
</tr>
<tr>
<td class="file low" data-value="src/sse"><a href="src/sse/index.html">src/sse</a></td>
<td data-value="0" class="pic low">
<div class="chart"><div class="cover-fill" style="width: 0%"></div><div class="cover-empty" style="width: 100%"></div></div>
</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="123" class="abs low">0/123</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="29" class="abs low">0/29</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="24" class="abs low">0/24</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="115" class="abs low">0/115</td>
</tr>
<tr>
<td class="file low" data-value="src/tools"><a href="src/tools/index.html">src/tools</a></td>
<td data-value="29.5" class="pic low">
<div class="chart"><div class="cover-fill" style="width: 29%"></div><div class="cover-empty" style="width: 71%"></div></div>
</td>
<td data-value="29.5" class="pct low">29.5%</td>
<td data-value="61" class="abs low">18/61</td>
<td data-value="22.22" class="pct low">22.22%</td>
<td data-value="18" class="abs low">4/18</td>
<td data-value="42.85" class="pct low">42.85%</td>
<td data-value="14" class="abs low">6/14</td>
<td data-value="29.31" class="pct low">29.31%</td>
<td data-value="58" class="abs low">17/58</td>
</tr> </tr>
<tr> <tr>
@@ -191,7 +311,7 @@
<div class='footer quiet pad2 space-top1 center small'> <div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a> <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2025-01-30T08:31:24.499Z at 2025-01-30T18:59:31.394Z
</div> </div>
<script src="prettify.js"></script> <script src="prettify.js"></script>
<script> <script>

File diff suppressed because it is too large Load Diff

View File

@@ -1,48 +1,39 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */ /** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = { module.exports = {
preset: 'ts-jest', preset: 'ts-jest/presets/default-esm',
testEnvironment: 'node', testEnvironment: 'node',
setupFiles: ['./jest.setup.ts'], extensionsToTreatAsEsm: ['.ts'],
moduleNameMapper: { moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1', '^(\\.{1,2}/.*)\\.js$': '$1',
'#(.*)': '<rootDir>/node_modules/$1', '#(.*)': '<rootDir>/node_modules/$1',
'^(\\.{1,2}/.*)\\.ts$': '$1', '^(\\.{1,2}/.*)\\.ts$': '$1',
'^chalk$': '<rootDir>/node_modules/chalk/source/index.js', '^chalk$': 'chalk',
'#ansi-styles': '<rootDir>/node_modules/ansi-styles/index.js', '#ansi-styles': 'ansi-styles',
'#supports-color': '<rootDir>/node_modules/supports-color/index.js' '#supports-color': 'supports-color'
}, },
transform: { transform: {
'^.+\\.tsx?$': ['ts-jest', { '^.+\\.tsx?$': [
'ts-jest',
{
useESM: true, useESM: true,
}], },
],
}, },
transformIgnorePatterns: [ transformIgnorePatterns: [
'node_modules/(?!(@digital-alchemy|chalk|#ansi-styles|#supports-color)/)' 'node_modules/(?!(@digital-alchemy|chalk|ansi-styles|supports-color)/)'
], ],
resolver: '<rootDir>/jest-resolver.cjs', resolver: '<rootDir>/jest-resolver.cjs',
testMatch: ['**/__tests__/**/*.test.ts'], setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
testMatch: [
'**/__tests__/helpers.test.ts',
'**/__tests__/schemas/devices.test.ts'
],
globals: { globals: {
'ts-jest': { 'ts-jest': {
useESM: true, useESM: true,
}, },
}, },
collectCoverage: true, collectCoverage: false,
coverageDirectory: 'coverage', verbose: true,
coverageReporters: ['text', 'lcov', 'clover', 'html'], testTimeout: 30000
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/**/*.test.ts',
'!src/types/**/*',
'!src/polyfills.ts'
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
},
verbose: true
}; };

View File

@@ -1,43 +1,21 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */ /** @type {import('jest').Config} */
module.exports = { module.exports = {
preset: 'ts-jest', preset: 'ts-jest',
testEnvironment: 'node', testEnvironment: 'node',
moduleNameMapper: { moduleNameMapper: {
'^@src/(.*)$': '<rootDir>/src/$1', '^(\\.{1,2}/.*)\\.js$': '$1',
'^@tests/(.*)$': '<rootDir>/__tests__/$1',
'^(\\.{1,2}/.*)\\.js$': '$1'
}, },
roots: [
'<rootDir>/src',
'<rootDir>/__tests__'
],
transform: { transform: {
'^.+\\.tsx?$': ['ts-jest', { '^.+\\.tsx?$': ['ts-jest', {
useESM: true, useESM: true
tsconfig: './tsconfig.json'
}] }]
}, },
extensionsToTreatAsEsm: ['.ts', '.tsx'], extensionsToTreatAsEsm: ['.ts'],
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$', setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], testMatch: ['**/__tests__/**/*.test.ts'],
resolver: '<rootDir>/jest-resolver.cjs', collectCoverage: true,
coverageDirectory: 'coverage',
transformIgnorePatterns: [ transformIgnorePatterns: [
'node_modules/(?!(@digital-alchemy|litemcp|semver|zod)/)' 'node_modules/(?!(chalk|#ansi-styles|#supports-color)/)'
], ]
modulePathIgnorePatterns: [
'<rootDir>/dist/'
],
testEnvironmentOptions: {
experimentalVmModules: true
},
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
globals: {
'ts-jest': {
useESM: true,
tsconfig: {
allowJs: true,
esModuleInterop: true
}
}
}
}; };

View File

@@ -1,15 +1,84 @@
import { config } from 'dotenv'; import { jest } from '@jest/globals';
import { resolve } from 'path'; import dotenv from 'dotenv';
import { TextEncoder, TextDecoder } from 'util';
// Load test environment variables // Load test environment variables
const envFile = process.env.NODE_ENV === 'test' ? '.env.test' : '.env'; dotenv.config({ path: '.env.test' });
config({ path: resolve(__dirname, envFile) });
// Set default test environment variables if not provided // Set test environment
process.env.TEST_HASS_HOST = process.env.TEST_HASS_HOST || 'http://localhost:8123';
process.env.TEST_HASS_TOKEN = process.env.TEST_HASS_TOKEN || 'test_token';
process.env.TEST_HASS_SOCKET_URL = process.env.TEST_HASS_SOCKET_URL || 'ws://localhost:8123/api/websocket';
process.env.TEST_PORT = process.env.TEST_PORT || '3001';
// Ensure test environment
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
process.env.HASS_URL = 'http://localhost:8123';
process.env.HASS_TOKEN = 'test_token';
process.env.CLAUDE_API_KEY = 'test_api_key';
process.env.CLAUDE_MODEL = 'test_model';
// Add TextEncoder and TextDecoder to global scope
Object.defineProperty(global, 'TextEncoder', {
value: TextEncoder,
writable: true
});
Object.defineProperty(global, 'TextDecoder', {
value: TextDecoder,
writable: true
});
// Configure console for tests
const originalConsole = { ...console };
global.console = {
...console,
log: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
info: jest.fn(),
debug: jest.fn(),
};
// Increase test timeout
jest.setTimeout(30000);
// Mock WebSocket
jest.mock('ws', () => {
return {
WebSocket: jest.fn().mockImplementation(() => ({
on: jest.fn(),
send: jest.fn(),
close: jest.fn(),
removeAllListeners: jest.fn()
}))
};
});
// Mock chalk
jest.mock('chalk', () => ({
default: {
red: (text: string) => text,
green: (text: string) => text,
yellow: (text: string) => text,
blue: (text: string) => text,
magenta: (text: string) => text,
cyan: (text: string) => text,
white: (text: string) => text,
gray: (text: string) => text,
grey: (text: string) => text,
black: (text: string) => text,
bold: (text: string) => text,
dim: (text: string) => text,
italic: (text: string) => text,
underline: (text: string) => text,
inverse: (text: string) => text,
hidden: (text: string) => text,
strikethrough: (text: string) => text,
visible: (text: string) => text,
}
}));
// Reset mocks between tests
beforeEach(() => {
jest.clearAllMocks();
});
// Cleanup after tests
afterEach(() => {
jest.clearAllTimers();
});

View File

@@ -6,7 +6,7 @@
"main": "dist/index.js", "main": "dist/index.js",
"scripts": { "scripts": {
"build": "npx tsc", "build": "npx tsc",
"start": "node dist/index.js", "start": "node dist/src/index.js",
"dev": "tsx watch src/index.ts", "dev": "tsx watch src/index.ts",
"test": "NODE_OPTIONS=--experimental-vm-modules jest --config=jest.config.cjs", "test": "NODE_OPTIONS=--experimental-vm-modules jest --config=jest.config.cjs",
"test:coverage": "NODE_OPTIONS=--experimental-vm-modules jest --config=jest.config.cjs --coverage", "test:coverage": "NODE_OPTIONS=--experimental-vm-modules jest --config=jest.config.cjs --coverage",
@@ -21,29 +21,30 @@
"dependencies": { "dependencies": {
"@digital-alchemy/core": "^24.11.4", "@digital-alchemy/core": "^24.11.4",
"@digital-alchemy/hass": "^24.11.4", "@digital-alchemy/hass": "^24.11.4",
"ajv": "^8.17.1", "ajv": "^8.12.0",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"express-rate-limit": "^7.5.0", "express": "^4.18.2",
"helmet": "^8.0.0", "express-rate-limit": "^7.1.5",
"helmet": "^7.1.0",
"litemcp": "^0.7.0", "litemcp": "^0.7.0",
"uuid": "^11.0.5", "uuid": "^9.0.1",
"ws": "^8.18.0", "ws": "^8.16.0",
"zod": "^3.22.4" "zod": "^3.22.4"
}, },
"devDependencies": { "devDependencies": {
"@types/ajv": "^0.0.5", "@types/ajv": "^1.0.0",
"@types/express": "^5.0.0", "@types/express": "^4.17.21",
"@types/express-rate-limit": "^5.1.3", "@types/express-rate-limit": "^6.0.0",
"@types/helmet": "^0.0.48", "@types/helmet": "^4.0.0",
"@types/jest": "^28.1.8", "@types/jest": "^29.5.14",
"@types/node": "^20.17.10", "@types/node": "^20.17.16",
"@types/uuid": "^10.0.0", "@types/uuid": "^9.0.8",
"@types/ws": "^8.5.14", "@types/ws": "^8.5.10",
"jest": "^28.1.3", "jest": "^29.7.0",
"semver": "^6.3.1", "rimraf": "^5.0.5",
"ts-jest": "^28.0.8", "ts-jest": "^29.1.2",
"tsx": "^4.19.2", "tsx": "^4.7.0",
"typescript": "^5.7.2" "typescript": "^5.3.3"
}, },
"author": "Jango Blockchained", "author": "Jango Blockchained",
"license": "MIT" "license": "MIT"

View File

@@ -83,3 +83,4 @@ export interface HassEvent {
user_id?: string; user_id?: string;
}; };
} }

View File

@@ -13,6 +13,8 @@
"declaration": true, "declaration": true,
"sourceMap": true, "sourceMap": true,
"allowJs": true, "allowJs": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"types": [ "types": [
"node", "node",
"jest" "jest"
@@ -39,6 +41,7 @@
], ],
"exclude": [ "exclude": [
"node_modules", "node_modules",
"dist" "**/__tests__/**/*.ts",
"**/*.test.ts"
] ]
} }