@coffeerunhobby/mcp-ha-connect 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +8 -0
- package/.env.example +149 -0
- package/.gitattributes +30 -0
- package/Dockerfile +33 -0
- package/LICENSE +21 -0
- package/README.md +394 -0
- package/dist/config.d.ts +33 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +183 -0
- package/dist/config.js.map +1 -0
- package/dist/haClient/automations.d.ts +67 -0
- package/dist/haClient/automations.d.ts.map +1 -0
- package/dist/haClient/automations.js +181 -0
- package/dist/haClient/automations.js.map +1 -0
- package/dist/haClient/config.d.ts +28 -0
- package/dist/haClient/config.d.ts.map +1 -0
- package/dist/haClient/config.js +38 -0
- package/dist/haClient/config.js.map +1 -0
- package/dist/haClient/devices.d.ts +219 -0
- package/dist/haClient/devices.d.ts.map +1 -0
- package/dist/haClient/devices.js +506 -0
- package/dist/haClient/devices.js.map +1 -0
- package/dist/haClient/entities.d.ts +35 -0
- package/dist/haClient/entities.d.ts.map +1 -0
- package/dist/haClient/entities.js +97 -0
- package/dist/haClient/entities.js.map +1 -0
- package/dist/haClient/events.d.ts +138 -0
- package/dist/haClient/events.d.ts.map +1 -0
- package/dist/haClient/events.js +344 -0
- package/dist/haClient/events.js.map +1 -0
- package/dist/haClient/history.d.ts +26 -0
- package/dist/haClient/history.d.ts.map +1 -0
- package/dist/haClient/history.js +54 -0
- package/dist/haClient/history.js.map +1 -0
- package/dist/haClient/index.d.ts +153 -0
- package/dist/haClient/index.d.ts.map +1 -0
- package/dist/haClient/index.js +223 -0
- package/dist/haClient/index.js.map +1 -0
- package/dist/haClient/request.d.ts +34 -0
- package/dist/haClient/request.d.ts.map +1 -0
- package/dist/haClient/request.js +93 -0
- package/dist/haClient/request.js.map +1 -0
- package/dist/haClient/services.d.ts +22 -0
- package/dist/haClient/services.d.ts.map +1 -0
- package/dist/haClient/services.js +39 -0
- package/dist/haClient/services.js.map +1 -0
- package/dist/haClient/states.d.ts +26 -0
- package/dist/haClient/states.d.ts.map +1 -0
- package/dist/haClient/states.js +53 -0
- package/dist/haClient/states.js.map +1 -0
- package/dist/haClient/updates.d.ts +22 -0
- package/dist/haClient/updates.d.ts.map +1 -0
- package/dist/haClient/updates.js +48 -0
- package/dist/haClient/updates.js.map +1 -0
- package/dist/haClient.d.ts +97 -0
- package/dist/haClient.d.ts.map +1 -0
- package/dist/haClient.js +344 -0
- package/dist/haClient.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +92 -0
- package/dist/index.js.map +1 -0
- package/dist/localAI/index.d.ts +37 -0
- package/dist/localAI/index.d.ts.map +1 -0
- package/dist/localAI/index.js +73 -0
- package/dist/localAI/index.js.map +1 -0
- package/dist/localAI/prompts.d.ts +20 -0
- package/dist/localAI/prompts.d.ts.map +1 -0
- package/dist/localAI/prompts.js +48 -0
- package/dist/localAI/prompts.js.map +1 -0
- package/dist/localAI/providers/base.d.ts +51 -0
- package/dist/localAI/providers/base.d.ts.map +1 -0
- package/dist/localAI/providers/base.js +80 -0
- package/dist/localAI/providers/base.js.map +1 -0
- package/dist/localAI/providers/ollama.d.ts +22 -0
- package/dist/localAI/providers/ollama.d.ts.map +1 -0
- package/dist/localAI/providers/ollama.js +63 -0
- package/dist/localAI/providers/ollama.js.map +1 -0
- package/dist/localAI/providers/openai.d.ts +23 -0
- package/dist/localAI/providers/openai.d.ts.map +1 -0
- package/dist/localAI/providers/openai.js +63 -0
- package/dist/localAI/providers/openai.js.map +1 -0
- package/dist/localAI/types.d.ts +61 -0
- package/dist/localAI/types.d.ts.map +1 -0
- package/dist/localAI/types.js +6 -0
- package/dist/localAI/types.js.map +1 -0
- package/dist/ollamaClient.d.ts +37 -0
- package/dist/ollamaClient.d.ts.map +1 -0
- package/dist/ollamaClient.js +110 -0
- package/dist/ollamaClient.js.map +1 -0
- package/dist/resources/index.d.ts +11 -0
- package/dist/resources/index.d.ts.map +1 -0
- package/dist/resources/index.js +238 -0
- package/dist/resources/index.js.map +1 -0
- package/dist/server/common.d.ts +9 -0
- package/dist/server/common.d.ts.map +1 -0
- package/dist/server/common.js +27 -0
- package/dist/server/common.js.map +1 -0
- package/dist/server/eventSubscription.d.ts +53 -0
- package/dist/server/eventSubscription.d.ts.map +1 -0
- package/dist/server/eventSubscription.js +257 -0
- package/dist/server/eventSubscription.js.map +1 -0
- package/dist/server/http.d.ts +12 -0
- package/dist/server/http.d.ts.map +1 -0
- package/dist/server/http.js +491 -0
- package/dist/server/http.js.map +1 -0
- package/dist/server/rateLimiter.d.ts +70 -0
- package/dist/server/rateLimiter.d.ts.map +1 -0
- package/dist/server/rateLimiter.js +156 -0
- package/dist/server/rateLimiter.js.map +1 -0
- package/dist/server/sse.d.ts +32 -0
- package/dist/server/sse.d.ts.map +1 -0
- package/dist/server/sse.js +76 -0
- package/dist/server/sse.js.map +1 -0
- package/dist/server/stdio.d.ts +8 -0
- package/dist/server/stdio.d.ts.map +1 -0
- package/dist/server/stdio.js +16 -0
- package/dist/server/stdio.js.map +1 -0
- package/dist/server/stream.d.ts +26 -0
- package/dist/server/stream.d.ts.map +1 -0
- package/dist/server/stream.js +81 -0
- package/dist/server/stream.js.map +1 -0
- package/dist/server.d.ts +8 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +30 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/callService.d.ts +8 -0
- package/dist/tools/callService.d.ts.map +1 -0
- package/dist/tools/callService.js +100 -0
- package/dist/tools/callService.js.map +1 -0
- package/dist/tools/getClient.d.ts +8 -0
- package/dist/tools/getClient.d.ts.map +1 -0
- package/dist/tools/getClient.js +94 -0
- package/dist/tools/getClient.js.map +1 -0
- package/dist/tools/getDevice.d.ts +8 -0
- package/dist/tools/getDevice.d.ts.map +1 -0
- package/dist/tools/getDevice.js +94 -0
- package/dist/tools/getDevice.js.map +1 -0
- package/dist/tools/getEntitiesByDomain.d.ts +8 -0
- package/dist/tools/getEntitiesByDomain.d.ts.map +1 -0
- package/dist/tools/getEntitiesByDomain.js +80 -0
- package/dist/tools/getEntitiesByDomain.js.map +1 -0
- package/dist/tools/getState.d.ts +8 -0
- package/dist/tools/getState.d.ts.map +1 -0
- package/dist/tools/getState.js +90 -0
- package/dist/tools/getState.js.map +1 -0
- package/dist/tools/getStates.d.ts +8 -0
- package/dist/tools/getStates.d.ts.map +1 -0
- package/dist/tools/getStates.js +70 -0
- package/dist/tools/getStates.js.map +1 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +9 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/listClients.d.ts +8 -0
- package/dist/tools/listClients.d.ts.map +1 -0
- package/dist/tools/listClients.js +73 -0
- package/dist/tools/listClients.js.map +1 -0
- package/dist/tools/listDevices.d.ts +8 -0
- package/dist/tools/listDevices.d.ts.map +1 -0
- package/dist/tools/listDevices.js +73 -0
- package/dist/tools/listDevices.js.map +1 -0
- package/dist/tools/listSites.d.ts +8 -0
- package/dist/tools/listSites.d.ts.map +1 -0
- package/dist/tools/listSites.js +67 -0
- package/dist/tools/listSites.js.map +1 -0
- package/dist/tools/registry.d.ts +9 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +1382 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/searchDevices.d.ts +8 -0
- package/dist/tools/searchDevices.d.ts.map +1 -0
- package/dist/tools/searchDevices.js +84 -0
- package/dist/tools/searchDevices.js.map +1 -0
- package/dist/tools/searchEntities.d.ts +8 -0
- package/dist/tools/searchEntities.d.ts.map +1 -0
- package/dist/tools/searchEntities.js +80 -0
- package/dist/tools/searchEntities.js.map +1 -0
- package/dist/types/entity.d.ts +98 -0
- package/dist/types/entity.d.ts.map +1 -0
- package/dist/types/entity.js +2 -0
- package/dist/types/entity.js.map +1 -0
- package/dist/types/errors.d.ts +16 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/errors.js +32 -0
- package/dist/types/errors.js.map +1 -0
- package/dist/types/haConfig.d.ts +8 -0
- package/dist/types/haConfig.d.ts.map +1 -0
- package/dist/types/haConfig.js +2 -0
- package/dist/types/haConfig.js.map +1 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/request.d.ts +7 -0
- package/dist/types/request.d.ts.map +1 -0
- package/dist/types/request.js +2 -0
- package/dist/types/request.js.map +1 -0
- package/dist/types/serviceCall.d.ts +18 -0
- package/dist/types/serviceCall.d.ts.map +1 -0
- package/dist/types/serviceCall.js +2 -0
- package/dist/types/serviceCall.js.map +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/config-validations.d.ts +12 -0
- package/dist/utils/config-validations.d.ts.map +1 -0
- package/dist/utils/config-validations.js +43 -0
- package/dist/utils/config-validations.js.map +1 -0
- package/dist/utils/logger.d.ts +24 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +94 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/wrapper.d.ts +8 -0
- package/dist/wrapper.d.ts.map +1 -0
- package/dist/wrapper.js +301 -0
- package/dist/wrapper.js.map +1 -0
- package/docker-compose.yml +17 -0
- package/docs/CHANGELOG.md +86 -0
- package/docs/LOCAL_CLIENT.md +268 -0
- package/docs/SSE_API.md +341 -0
- package/eslint.config.js +33 -0
- package/n8n/ai-agent-safety-monitor.json +355 -0
- package/openapi.json +166 -0
- package/package.json +51 -0
- package/src/config.ts +260 -0
- package/src/haClient/automations.ts +217 -0
- package/src/haClient/config.ts +41 -0
- package/src/haClient/devices.ts +653 -0
- package/src/haClient/entities.ts +128 -0
- package/src/haClient/events.ts +456 -0
- package/src/haClient/history.ts +68 -0
- package/src/haClient/index.ts +287 -0
- package/src/haClient/request.ts +127 -0
- package/src/haClient/services.ts +45 -0
- package/src/haClient/states.ts +60 -0
- package/src/haClient/updates.ts +58 -0
- package/src/index.ts +99 -0
- package/src/localAI/index.ts +88 -0
- package/src/localAI/prompts.ts +50 -0
- package/src/localAI/providers/base.ts +120 -0
- package/src/localAI/providers/ollama.ts +78 -0
- package/src/localAI/providers/openai.ts +86 -0
- package/src/localAI/types.ts +67 -0
- package/src/resources/index.ts +266 -0
- package/src/server/common.ts +38 -0
- package/src/server/eventSubscription.ts +318 -0
- package/src/server/http.ts +546 -0
- package/src/server/rateLimiter.ts +195 -0
- package/src/server/sse.ts +120 -0
- package/src/server/stdio.ts +22 -0
- package/src/server/stream.ts +113 -0
- package/src/tools/callService.ts +114 -0
- package/src/tools/getEntitiesByDomain.ts +92 -0
- package/src/tools/getState.ts +103 -0
- package/src/tools/getStates.ts +80 -0
- package/src/tools/index.ts +9 -0
- package/src/tools/registry.ts +1521 -0
- package/src/tools/searchEntities.ts +92 -0
- package/src/types/entity.ts +110 -0
- package/src/types/errors.ts +33 -0
- package/src/types/haConfig.ts +7 -0
- package/src/types/index.ts +5 -0
- package/src/types/request.ts +6 -0
- package/src/types/serviceCall.ts +18 -0
- package/src/types.ts +2 -0
- package/src/utils/config-validations.ts +46 -0
- package/src/utils/logger.ts +114 -0
- package/src/wrapper.ts +340 -0
- package/tests/config.test.ts +332 -0
- package/tests/haClient/automations.test.ts +133 -0
- package/tests/haClient/config.test.ts +96 -0
- package/tests/haClient/entities.test.ts +188 -0
- package/tests/haClient/history.test.ts +170 -0
- package/tests/haClient/index.test.ts +221 -0
- package/tests/haClient/request.test.ts +217 -0
- package/tests/haClient/services.test.ts +120 -0
- package/tests/haClient/states.test.ts +119 -0
- package/tests/haClient/updates.test.ts +193 -0
- package/tests/index.test.ts +164 -0
- package/tests/localAI/index.test.ts +168 -0
- package/tests/localAI/ollama.test.ts +187 -0
- package/tests/localAI/openai.test.ts +257 -0
- package/tests/server/common.test.ts +64 -0
- package/tests/server/http.test.ts +326 -0
- package/tests/server/sse.test.ts +240 -0
- package/tests/server/stdio.test.ts +102 -0
- package/tests/server/stream.test.ts +287 -0
- package/tests/tools/registry.test.ts +355 -0
- package/tests/utils/config-validations.test.ts +289 -0
- package/tests/utils/logger.test.ts +327 -0
- package/tsconfig.json +25 -0
- package/vitest.config.ts +26 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP server tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
6
|
+
import type { HaClient } from '../../src/haClient/index.js';
|
|
7
|
+
import type { EnvironmentConfig } from '../../src/config.js';
|
|
8
|
+
|
|
9
|
+
// Mock dependencies
|
|
10
|
+
vi.mock('node:http');
|
|
11
|
+
vi.mock('../../src/server/sse.js');
|
|
12
|
+
vi.mock('../../src/server/stream.js');
|
|
13
|
+
vi.mock('../../src/utils/logger.js');
|
|
14
|
+
|
|
15
|
+
describe('HTTP Server', () => {
|
|
16
|
+
let mockClient: HaClient;
|
|
17
|
+
let mockConfig: EnvironmentConfig;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
vi.clearAllMocks();
|
|
21
|
+
|
|
22
|
+
mockClient = {
|
|
23
|
+
checkApi: vi.fn().mockResolvedValue({ message: 'API running.' }),
|
|
24
|
+
getStates: vi.fn(),
|
|
25
|
+
getState: vi.fn(),
|
|
26
|
+
callService: vi.fn(),
|
|
27
|
+
getEntitiesByDomain: vi.fn(),
|
|
28
|
+
searchEntities: vi.fn(),
|
|
29
|
+
} as any;
|
|
30
|
+
|
|
31
|
+
mockConfig = {
|
|
32
|
+
baseUrl: 'http://homeassistant.10.0.0.19.nip.io:8123',
|
|
33
|
+
token: 'test-token',
|
|
34
|
+
strictSsl: false,
|
|
35
|
+
timeout: 30000,
|
|
36
|
+
aiProvider: 'ollama',
|
|
37
|
+
aiUrl: 'http://ollama.10.0.0.17.nip.io:11434',
|
|
38
|
+
aiModel: 'qwen3:14b',
|
|
39
|
+
aiTimeout: 60000,
|
|
40
|
+
logLevel: 'info',
|
|
41
|
+
logFormat: 'plain',
|
|
42
|
+
useHttp: true,
|
|
43
|
+
stateful: false,
|
|
44
|
+
httpPort: 3000,
|
|
45
|
+
httpTransport: 'stream',
|
|
46
|
+
httpBindAddr: '127.0.0.1',
|
|
47
|
+
httpPath: '/mcp',
|
|
48
|
+
httpEnableHealthcheck: true,
|
|
49
|
+
httpHealthcheckPath: '/health',
|
|
50
|
+
httpAllowCors: true,
|
|
51
|
+
httpAllowedOrigins: ['127.0.0.1', 'localhost'],
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('Health Check Handler', () => {
|
|
56
|
+
it('should return health status for stream transport', () => {
|
|
57
|
+
const expectedResponse = {
|
|
58
|
+
status: 'healthy',
|
|
59
|
+
version: '0.1.0',
|
|
60
|
+
transport: 'stream',
|
|
61
|
+
stateful: false,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
expect(expectedResponse.status).toBe('healthy');
|
|
65
|
+
expect(expectedResponse.transport).toBe('stream');
|
|
66
|
+
expect(expectedResponse.stateful).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should return health status for SSE transport', () => {
|
|
70
|
+
const sseConfig = { ...mockConfig, httpTransport: 'sse' as const };
|
|
71
|
+
|
|
72
|
+
const expectedResponse = {
|
|
73
|
+
status: 'healthy',
|
|
74
|
+
version: '0.1.0',
|
|
75
|
+
transport: sseConfig.httpTransport,
|
|
76
|
+
stateful: sseConfig.stateful,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
expect(expectedResponse.transport).toBe('sse');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should include stateful flag when enabled', () => {
|
|
83
|
+
const statefulConfig = { ...mockConfig, stateful: true };
|
|
84
|
+
|
|
85
|
+
const expectedResponse = {
|
|
86
|
+
status: 'healthy',
|
|
87
|
+
version: '0.1.0',
|
|
88
|
+
transport: statefulConfig.httpTransport,
|
|
89
|
+
stateful: statefulConfig.stateful,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
expect(expectedResponse.stateful).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('CORS Handler', () => {
|
|
97
|
+
it('should not set CORS headers when CORS is disabled', () => {
|
|
98
|
+
const noCorsConfig = { ...mockConfig, httpAllowCors: false };
|
|
99
|
+
|
|
100
|
+
expect(noCorsConfig.httpAllowCors).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should allow specific origins', () => {
|
|
104
|
+
const allowedOrigins = ['http://mcpserver.10.0.0.18.nip.io:3000', 'http://192.168.0.10:8080'];
|
|
105
|
+
const corsConfig = { ...mockConfig, httpAllowedOrigins: allowedOrigins };
|
|
106
|
+
|
|
107
|
+
expect(corsConfig.httpAllowedOrigins).toContain('http://mcpserver.10.0.0.18.nip.io:3000');
|
|
108
|
+
expect(corsConfig.httpAllowedOrigins).toContain('http://192.168.0.10:8080');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should allow all origins when list is empty (wildcard)', () => {
|
|
112
|
+
const wildcardConfig = { ...mockConfig, httpAllowedOrigins: [] };
|
|
113
|
+
|
|
114
|
+
// Empty array means wildcard was used
|
|
115
|
+
expect(wildcardConfig.httpAllowedOrigins).toEqual([]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should set correct CORS headers', () => {
|
|
119
|
+
const expectedHeaders = {
|
|
120
|
+
'Access-Control-Allow-Origin': 'http://mcpserver.10.0.0.18.nip.io:3000',
|
|
121
|
+
'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
|
|
122
|
+
'Access-Control-Allow-Headers': 'Content-Type, Mcp-Session-Id',
|
|
123
|
+
'Access-Control-Max-Age': '86400',
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
expect(expectedHeaders['Access-Control-Allow-Methods']).toContain('GET');
|
|
127
|
+
expect(expectedHeaders['Access-Control-Allow-Methods']).toContain('POST');
|
|
128
|
+
expect(expectedHeaders['Access-Control-Allow-Headers']).toContain('Mcp-Session-Id');
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('Server Configuration', () => {
|
|
133
|
+
it('should use default port if not specified', () => {
|
|
134
|
+
const configWithoutPort = { ...mockConfig };
|
|
135
|
+
delete configWithoutPort.httpPort;
|
|
136
|
+
|
|
137
|
+
const port = configWithoutPort.httpPort ?? 3000;
|
|
138
|
+
expect(port).toBe(3000);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should use default bind address if not specified', () => {
|
|
142
|
+
const configWithoutBind = { ...mockConfig };
|
|
143
|
+
delete configWithoutBind.httpBindAddr;
|
|
144
|
+
|
|
145
|
+
const bindAddr = configWithoutBind.httpBindAddr ?? '127.0.0.1';
|
|
146
|
+
expect(bindAddr).toBe('127.0.0.1');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should use default health check path if not specified', () => {
|
|
150
|
+
const configWithoutHealthPath = { ...mockConfig };
|
|
151
|
+
delete configWithoutHealthPath.httpHealthcheckPath;
|
|
152
|
+
|
|
153
|
+
const healthPath = configWithoutHealthPath.httpHealthcheckPath ?? '/health';
|
|
154
|
+
expect(healthPath).toBe('/health');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should support custom configuration', () => {
|
|
158
|
+
const customConfig = {
|
|
159
|
+
...mockConfig,
|
|
160
|
+
httpPort: 8080,
|
|
161
|
+
httpBindAddr: '0.0.0.0',
|
|
162
|
+
httpPath: '/custom-mcp',
|
|
163
|
+
httpHealthcheckPath: '/status',
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
expect(customConfig.httpPort).toBe(8080);
|
|
167
|
+
expect(customConfig.httpBindAddr).toBe('0.0.0.0');
|
|
168
|
+
expect(customConfig.httpPath).toBe('/custom-mcp');
|
|
169
|
+
expect(customConfig.httpHealthcheckPath).toBe('/status');
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('Request Routing', () => {
|
|
174
|
+
it('should route to health check for /health', () => {
|
|
175
|
+
const healthPath = mockConfig.httpHealthcheckPath;
|
|
176
|
+
const requestUrl = '/health';
|
|
177
|
+
|
|
178
|
+
expect(requestUrl).toBe(healthPath);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should route to MCP endpoint for stream transport', () => {
|
|
182
|
+
const mcpPath = mockConfig.httpPath;
|
|
183
|
+
const requestUrl = '/mcp';
|
|
184
|
+
|
|
185
|
+
expect(requestUrl).toBe(mcpPath);
|
|
186
|
+
expect(mockConfig.httpTransport).toBe('stream');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should route to SSE endpoint for SSE transport', () => {
|
|
190
|
+
const sseConfig = { ...mockConfig, httpTransport: 'sse' as const, httpPath: '/sse' };
|
|
191
|
+
const requestUrl = '/sse';
|
|
192
|
+
|
|
193
|
+
expect(requestUrl).toBe(sseConfig.httpPath);
|
|
194
|
+
expect(sseConfig.httpTransport).toBe('sse');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should handle OPTIONS requests for CORS preflight', () => {
|
|
198
|
+
const method = 'OPTIONS';
|
|
199
|
+
|
|
200
|
+
expect(method).toBe('OPTIONS');
|
|
201
|
+
// Should return 204 No Content
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should return 404 for unknown paths', () => {
|
|
205
|
+
const unknownPath = '/unknown-endpoint';
|
|
206
|
+
const validPaths = [mockConfig.httpPath, mockConfig.httpHealthcheckPath];
|
|
207
|
+
|
|
208
|
+
expect(validPaths).not.toContain(unknownPath);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe('Session Management', () => {
|
|
213
|
+
it('should create new session when stateful mode is disabled', () => {
|
|
214
|
+
expect(mockConfig.stateful).toBe(false);
|
|
215
|
+
// Each request should create a new transport
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should reuse session when stateful mode is enabled', () => {
|
|
219
|
+
const statefulConfig = { ...mockConfig, stateful: true };
|
|
220
|
+
|
|
221
|
+
expect(statefulConfig.stateful).toBe(true);
|
|
222
|
+
// Should maintain sessions in Map
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should require Mcp-Session-Id header for stateful SSE', () => {
|
|
226
|
+
const statefulSseConfig = {
|
|
227
|
+
...mockConfig,
|
|
228
|
+
httpTransport: 'sse' as const,
|
|
229
|
+
stateful: true,
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
expect(statefulSseConfig.stateful).toBe(true);
|
|
233
|
+
expect(statefulSseConfig.httpTransport).toBe('sse');
|
|
234
|
+
// Should validate session ID from header
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should return error for invalid session ID', () => {
|
|
238
|
+
const statefulConfig = { ...mockConfig, stateful: true };
|
|
239
|
+
|
|
240
|
+
expect(statefulConfig.stateful).toBe(true);
|
|
241
|
+
// Should return 400 error for missing/invalid session
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe('Error Handling', () => {
|
|
246
|
+
it('should handle request parsing errors', () => {
|
|
247
|
+
const invalidJson = 'not-valid-json';
|
|
248
|
+
|
|
249
|
+
expect(() => JSON.parse(invalidJson)).toThrow();
|
|
250
|
+
// Should return 500 with error message
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('should not send headers if already sent', () => {
|
|
254
|
+
let headersSent = true;
|
|
255
|
+
|
|
256
|
+
if (!headersSent) {
|
|
257
|
+
// Would send error response
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
expect(headersSent).toBe(true);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('should log errors with request details', () => {
|
|
264
|
+
const error = new Error('Test error');
|
|
265
|
+
const requestMethod = 'POST';
|
|
266
|
+
const requestUrl = '/mcp';
|
|
267
|
+
|
|
268
|
+
const logData = {
|
|
269
|
+
error,
|
|
270
|
+
method: requestMethod,
|
|
271
|
+
url: requestUrl,
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
expect(logData.method).toBe('POST');
|
|
275
|
+
expect(logData.url).toBe('/mcp');
|
|
276
|
+
expect(logData.error).toBe(error);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe('JSON Response Helper', () => {
|
|
281
|
+
it('should format JSON response correctly', () => {
|
|
282
|
+
const statusCode = 200;
|
|
283
|
+
const data = { status: 'healthy', version: '0.1.0' };
|
|
284
|
+
|
|
285
|
+
const expectedContentType = 'application/json';
|
|
286
|
+
const expectedBody = JSON.stringify(data);
|
|
287
|
+
|
|
288
|
+
expect(expectedContentType).toBe('application/json');
|
|
289
|
+
expect(expectedBody).toBe('{"status":"healthy","version":"0.1.0"}');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('should handle error responses', () => {
|
|
293
|
+
const statusCode = 500;
|
|
294
|
+
const errorData = {
|
|
295
|
+
error: 'Internal server error',
|
|
296
|
+
message: 'Something went wrong',
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const expectedBody = JSON.stringify(errorData);
|
|
300
|
+
|
|
301
|
+
expect(statusCode).toBe(500);
|
|
302
|
+
expect(JSON.parse(expectedBody).error).toBe('Internal server error');
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
describe('Body Parsing', () => {
|
|
307
|
+
it('should parse valid JSON body', () => {
|
|
308
|
+
const jsonBody = '{"key":"value"}';
|
|
309
|
+
const parsed = JSON.parse(jsonBody);
|
|
310
|
+
|
|
311
|
+
expect(parsed).toEqual({ key: 'value' });
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('should return undefined for empty body', () => {
|
|
315
|
+
const emptyBody = '';
|
|
316
|
+
|
|
317
|
+
expect(emptyBody || undefined).toBeUndefined();
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('should throw error for invalid JSON', () => {
|
|
321
|
+
const invalidJson = '{invalid}';
|
|
322
|
+
|
|
323
|
+
expect(() => JSON.parse(invalidJson)).toThrow();
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
});
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSE (Server-Sent Events) transport tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
6
|
+
import type { HaClient } from '../../src/haClient/index.js';
|
|
7
|
+
import type { EnvironmentConfig } from '../../src/config.js';
|
|
8
|
+
|
|
9
|
+
// Mock dependencies
|
|
10
|
+
vi.mock('@modelcontextprotocol/sdk/server/sse.js');
|
|
11
|
+
vi.mock('../../src/server/common.js');
|
|
12
|
+
vi.mock('../../src/utils/logger.js');
|
|
13
|
+
|
|
14
|
+
describe('SSE Transport', () => {
|
|
15
|
+
let mockClient: HaClient;
|
|
16
|
+
let mockConfig: EnvironmentConfig;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
vi.clearAllMocks();
|
|
20
|
+
|
|
21
|
+
mockClient = {
|
|
22
|
+
checkApi: vi.fn().mockResolvedValue({ message: 'API running.' }),
|
|
23
|
+
getStates: vi.fn(),
|
|
24
|
+
getState: vi.fn(),
|
|
25
|
+
callService: vi.fn(),
|
|
26
|
+
getEntitiesByDomain: vi.fn(),
|
|
27
|
+
searchEntities: vi.fn(),
|
|
28
|
+
} as any;
|
|
29
|
+
|
|
30
|
+
mockConfig = {
|
|
31
|
+
baseUrl: 'http://homeassistant.10.0.0.19.nip.io:8123',
|
|
32
|
+
token: 'test-token',
|
|
33
|
+
strictSsl: false,
|
|
34
|
+
timeout: 30000,
|
|
35
|
+
aiProvider: 'ollama',
|
|
36
|
+
aiUrl: 'http://ollama.10.0.0.17.nip.io:11434',
|
|
37
|
+
aiModel: 'qwen3:14b',
|
|
38
|
+
aiTimeout: 60000,
|
|
39
|
+
logLevel: 'info',
|
|
40
|
+
logFormat: 'plain',
|
|
41
|
+
useHttp: true,
|
|
42
|
+
stateful: true,
|
|
43
|
+
httpPort: 3000,
|
|
44
|
+
httpTransport: 'sse',
|
|
45
|
+
httpBindAddr: '127.0.0.1',
|
|
46
|
+
httpPath: '/sse',
|
|
47
|
+
httpEnableHealthcheck: true,
|
|
48
|
+
httpHealthcheckPath: '/health',
|
|
49
|
+
httpAllowCors: true,
|
|
50
|
+
httpAllowedOrigins: ['127.0.0.1', 'localhost'],
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('getSseMessagePath', () => {
|
|
55
|
+
it('should return correct message path', async () => {
|
|
56
|
+
const { getSseMessagePath } = await import('../../src/server/sse.js');
|
|
57
|
+
|
|
58
|
+
const messagePath = getSseMessagePath();
|
|
59
|
+
expect(messagePath).toBe('/messages');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('SSE Connection', () => {
|
|
64
|
+
it('should create SSE transport with correct configuration', () => {
|
|
65
|
+
const endpoint = '/sse';
|
|
66
|
+
|
|
67
|
+
expect(mockConfig.httpTransport).toBe('sse');
|
|
68
|
+
expect(mockConfig.httpPath).toBe(endpoint);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should generate session ID for stateful connections', () => {
|
|
72
|
+
const sessionId = `sse-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
73
|
+
|
|
74
|
+
expect(sessionId).toMatch(/^sse-\d+-[a-z0-9]+$/);
|
|
75
|
+
expect(sessionId.length).toBeGreaterThan(10);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should set correct SSE headers', () => {
|
|
79
|
+
const sseHeaders = {
|
|
80
|
+
'Content-Type': 'text/event-stream',
|
|
81
|
+
'Cache-Control': 'no-cache',
|
|
82
|
+
'Connection': 'keep-alive',
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
expect(sseHeaders['Content-Type']).toBe('text/event-stream');
|
|
86
|
+
expect(sseHeaders['Cache-Control']).toBe('no-cache');
|
|
87
|
+
expect(sseHeaders['Connection']).toBe('keep-alive');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should include session ID in stateful mode', () => {
|
|
91
|
+
expect(mockConfig.stateful).toBe(true);
|
|
92
|
+
|
|
93
|
+
const sessionId = 'sse-123456-abc';
|
|
94
|
+
const sseHeaders = {
|
|
95
|
+
'Mcp-Session-Id': sessionId,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
expect(sseHeaders['Mcp-Session-Id']).toBe(sessionId);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('SSE Message Handling', () => {
|
|
103
|
+
it('should require session ID for stateful mode', () => {
|
|
104
|
+
expect(mockConfig.stateful).toBe(true);
|
|
105
|
+
|
|
106
|
+
const sessionId = undefined;
|
|
107
|
+
const isValid = !!sessionId;
|
|
108
|
+
|
|
109
|
+
expect(isValid).toBe(false);
|
|
110
|
+
// Should return 400 error
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should validate session exists', () => {
|
|
114
|
+
const sessions = new Map();
|
|
115
|
+
const sessionId = 'sse-123';
|
|
116
|
+
const session = sessions.get(sessionId);
|
|
117
|
+
|
|
118
|
+
expect(session).toBeUndefined();
|
|
119
|
+
// Should return 400 error for invalid session
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should handle valid session', () => {
|
|
123
|
+
const sessions = new Map();
|
|
124
|
+
const sessionId = 'sse-123';
|
|
125
|
+
const mockSession = {
|
|
126
|
+
transport: { sessionId },
|
|
127
|
+
server: {},
|
|
128
|
+
};
|
|
129
|
+
sessions.set(sessionId, mockSession);
|
|
130
|
+
|
|
131
|
+
const session = sessions.get(sessionId);
|
|
132
|
+
|
|
133
|
+
expect(session).toBeDefined();
|
|
134
|
+
expect(session?.transport.sessionId).toBe(sessionId);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should parse message body', () => {
|
|
138
|
+
const messageBody = {
|
|
139
|
+
jsonrpc: '2.0',
|
|
140
|
+
id: 1,
|
|
141
|
+
method: 'tools/call',
|
|
142
|
+
params: {
|
|
143
|
+
name: 'getStates',
|
|
144
|
+
arguments: {},
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
expect(messageBody.method).toBe('tools/call');
|
|
149
|
+
expect(messageBody.params.name).toBe('getStates');
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('SSE Protocol', () => {
|
|
154
|
+
it('should use legacy MCP protocol version', () => {
|
|
155
|
+
const protocolVersion = '2024-11-05';
|
|
156
|
+
|
|
157
|
+
expect(protocolVersion).toBe('2024-11-05');
|
|
158
|
+
// This is the legacy SSE protocol version
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should support text/event-stream content type', () => {
|
|
162
|
+
const contentType = 'text/event-stream';
|
|
163
|
+
|
|
164
|
+
expect(contentType).toBe('text/event-stream');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should maintain persistent connection', () => {
|
|
168
|
+
const connection = 'keep-alive';
|
|
169
|
+
|
|
170
|
+
expect(connection).toBe('keep-alive');
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe('Error Handling', () => {
|
|
175
|
+
it('should handle missing session gracefully', () => {
|
|
176
|
+
const sessionId = undefined;
|
|
177
|
+
const errorResponse = {
|
|
178
|
+
error: 'Invalid or missing session',
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
expect(sessionId).toBeUndefined();
|
|
182
|
+
expect(errorResponse.error).toBe('Invalid or missing session');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should handle invalid session ID', () => {
|
|
186
|
+
const sessions = new Map();
|
|
187
|
+
const invalidSessionId = 'invalid-session';
|
|
188
|
+
const session = sessions.get(invalidSessionId);
|
|
189
|
+
|
|
190
|
+
expect(session).toBeUndefined();
|
|
191
|
+
// Should return 400 error
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should handle malformed message body', () => {
|
|
195
|
+
const invalidJson = 'not-json';
|
|
196
|
+
|
|
197
|
+
expect(() => JSON.parse(invalidJson)).toThrow();
|
|
198
|
+
// Should return error response
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe('Transport State', () => {
|
|
203
|
+
it('should store transport and server in state', () => {
|
|
204
|
+
const state = {
|
|
205
|
+
transport: { sessionId: 'sse-123' },
|
|
206
|
+
server: { connect: vi.fn() },
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
expect(state.transport).toBeDefined();
|
|
210
|
+
expect(state.server).toBeDefined();
|
|
211
|
+
expect(state.transport.sessionId).toBe('sse-123');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should validate transport has sessionId property', () => {
|
|
215
|
+
const state = {
|
|
216
|
+
transport: { sessionId: 'sse-123' },
|
|
217
|
+
server: {},
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const hasSessionId = 'sessionId' in state.transport;
|
|
221
|
+
|
|
222
|
+
expect(hasSessionId).toBe(true);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should distinguish SSE from Stream transport', () => {
|
|
226
|
+
const sseState = {
|
|
227
|
+
transport: { sessionId: 'sse-123' },
|
|
228
|
+
server: {},
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const streamState = {
|
|
232
|
+
transport: {}, // No sessionId
|
|
233
|
+
server: {},
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
expect('sessionId' in sseState.transport).toBe(true);
|
|
237
|
+
expect('sessionId' in streamState.transport).toBe(false);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stdio server tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
6
|
+
import type { HaClient } from '../../src/haClient/index.js';
|
|
7
|
+
import type { EnvironmentConfig } from '../../src/config.js';
|
|
8
|
+
|
|
9
|
+
// Mock dependencies
|
|
10
|
+
vi.mock('../../src/server/common.js');
|
|
11
|
+
vi.mock('@modelcontextprotocol/sdk/server/stdio.js');
|
|
12
|
+
vi.mock('../../src/utils/logger.js');
|
|
13
|
+
|
|
14
|
+
describe('startStdioServer', () => {
|
|
15
|
+
let mockClient: HaClient;
|
|
16
|
+
let mockConfig: EnvironmentConfig;
|
|
17
|
+
let startStdioServer: any;
|
|
18
|
+
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
vi.clearAllMocks();
|
|
21
|
+
vi.resetModules();
|
|
22
|
+
|
|
23
|
+
mockConfig = {
|
|
24
|
+
baseUrl: 'http://homeassistant.10.0.0.19.nip.io:8123',
|
|
25
|
+
token: 'test-token',
|
|
26
|
+
strictSsl: false,
|
|
27
|
+
timeout: 30000,
|
|
28
|
+
aiProvider: 'ollama',
|
|
29
|
+
aiUrl: 'http://ollama.10.0.0.17.nip.io:11434',
|
|
30
|
+
aiModel: 'qwen3:14b',
|
|
31
|
+
aiTimeout: 60000,
|
|
32
|
+
logLevel: 'info',
|
|
33
|
+
logFormat: 'plain',
|
|
34
|
+
useHttp: false,
|
|
35
|
+
stateful: false,
|
|
36
|
+
httpTransport: 'stream',
|
|
37
|
+
httpEnableHealthcheck: true,
|
|
38
|
+
httpAllowCors: true,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Mock HaClient
|
|
42
|
+
mockClient = {
|
|
43
|
+
checkApi: vi.fn().mockResolvedValue({ message: 'API running.' }),
|
|
44
|
+
getStates: vi.fn(),
|
|
45
|
+
getState: vi.fn(),
|
|
46
|
+
callService: vi.fn(),
|
|
47
|
+
getEntitiesByDomain: vi.fn(),
|
|
48
|
+
searchEntities: vi.fn(),
|
|
49
|
+
} as any;
|
|
50
|
+
|
|
51
|
+
// Import the function to test
|
|
52
|
+
const stdioModule = await import('../../src/server/stdio.js');
|
|
53
|
+
startStdioServer = stdioModule.startStdioServer;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should create and start stdio server', async () => {
|
|
57
|
+
const { createServer } = await import('../../src/server/common.js');
|
|
58
|
+
const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
|
|
59
|
+
|
|
60
|
+
const mockServer = {
|
|
61
|
+
connect: vi.fn().mockResolvedValue(undefined),
|
|
62
|
+
};
|
|
63
|
+
vi.mocked(createServer).mockReturnValue(mockServer as any);
|
|
64
|
+
|
|
65
|
+
const mockTransport = {};
|
|
66
|
+
vi.mocked(StdioServerTransport).mockImplementation(() => mockTransport as any);
|
|
67
|
+
|
|
68
|
+
await startStdioServer(mockClient);
|
|
69
|
+
|
|
70
|
+
// Second argument (ollamaClient) is optional
|
|
71
|
+
expect(createServer).toHaveBeenCalledWith(mockClient, undefined);
|
|
72
|
+
expect(StdioServerTransport).toHaveBeenCalled();
|
|
73
|
+
expect(mockServer.connect).toHaveBeenCalledWith(mockTransport);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should log server startup', async () => {
|
|
77
|
+
const { logger } = await import('../../src/utils/logger.js');
|
|
78
|
+
const { createServer } = await import('../../src/server/common.js');
|
|
79
|
+
|
|
80
|
+
const mockServer = {
|
|
81
|
+
connect: vi.fn().mockResolvedValue(undefined),
|
|
82
|
+
};
|
|
83
|
+
vi.mocked(createServer).mockReturnValue(mockServer as any);
|
|
84
|
+
|
|
85
|
+
await startStdioServer(mockClient);
|
|
86
|
+
|
|
87
|
+
expect(logger.info).toHaveBeenCalledWith('Starting stdio server');
|
|
88
|
+
expect(logger.info).toHaveBeenCalledWith('Connecting stdio server');
|
|
89
|
+
expect(logger.info).toHaveBeenCalledWith('Stdio server connected and ready');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should handle connection errors', async () => {
|
|
93
|
+
const { createServer } = await import('../../src/server/common.js');
|
|
94
|
+
|
|
95
|
+
const mockServer = {
|
|
96
|
+
connect: vi.fn().mockRejectedValue(new Error('Connection failed')),
|
|
97
|
+
};
|
|
98
|
+
vi.mocked(createServer).mockReturnValue(mockServer as any);
|
|
99
|
+
|
|
100
|
+
await expect(startStdioServer(mockClient)).rejects.toThrow('Connection failed');
|
|
101
|
+
});
|
|
102
|
+
});
|