@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,187 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { OllamaProvider } from '../../src/localAI/providers/ollama.js';
|
|
3
|
+
import type { AIProviderConfig, SensorData } from '../../src/localAI/types.js';
|
|
4
|
+
|
|
5
|
+
// Mock fetch globally
|
|
6
|
+
const mockFetch = vi.fn();
|
|
7
|
+
global.fetch = mockFetch;
|
|
8
|
+
|
|
9
|
+
describe('OllamaProvider', () => {
|
|
10
|
+
const defaultConfig: AIProviderConfig = {
|
|
11
|
+
provider: 'ollama',
|
|
12
|
+
baseUrl: 'http://ollama.10.0.0.17.nip.io:11434',
|
|
13
|
+
model: 'qwen3:14b',
|
|
14
|
+
timeout: 60000,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
vi.clearAllMocks();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('getProviderName', () => {
|
|
22
|
+
it('should return "ollama"', () => {
|
|
23
|
+
const provider = new OllamaProvider(defaultConfig);
|
|
24
|
+
expect(provider.getProviderName()).toBe('ollama');
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('checkHealth', () => {
|
|
29
|
+
it('should return true when Ollama is available', async () => {
|
|
30
|
+
mockFetch.mockResolvedValueOnce({ ok: true });
|
|
31
|
+
|
|
32
|
+
const provider = new OllamaProvider(defaultConfig);
|
|
33
|
+
const result = await provider.checkHealth();
|
|
34
|
+
|
|
35
|
+
expect(result).toBe(true);
|
|
36
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
37
|
+
'http://ollama.10.0.0.17.nip.io:11434/api/tags',
|
|
38
|
+
expect.objectContaining({
|
|
39
|
+
signal: expect.any(AbortSignal),
|
|
40
|
+
})
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should return false when Ollama is not available', async () => {
|
|
45
|
+
mockFetch.mockRejectedValueOnce(new Error('Connection refused'));
|
|
46
|
+
|
|
47
|
+
const provider = new OllamaProvider(defaultConfig);
|
|
48
|
+
const result = await provider.checkHealth();
|
|
49
|
+
|
|
50
|
+
expect(result).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should return false when Ollama returns non-ok response', async () => {
|
|
54
|
+
mockFetch.mockResolvedValueOnce({ ok: false });
|
|
55
|
+
|
|
56
|
+
const provider = new OllamaProvider(defaultConfig);
|
|
57
|
+
const result = await provider.checkHealth();
|
|
58
|
+
|
|
59
|
+
expect(result).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('analyzeSensors', () => {
|
|
64
|
+
const sensorData: SensorData = {
|
|
65
|
+
'sensor.temperature': {
|
|
66
|
+
state: '45',
|
|
67
|
+
unit: '°C',
|
|
68
|
+
attributes: { friendly_name: 'Indoor Temperature' },
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
it('should analyze sensors successfully', async () => {
|
|
73
|
+
const mockResponse = {
|
|
74
|
+
severity: 'critical',
|
|
75
|
+
issue: 'High temperature detected',
|
|
76
|
+
reasoning: 'Temperature is above 40°C',
|
|
77
|
+
recommended_actions: ['Check HVAC', 'Open windows'],
|
|
78
|
+
ssh_diagnostics: ['systemctl status hvac'],
|
|
79
|
+
confidence: 0.95,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
mockFetch.mockResolvedValueOnce({
|
|
83
|
+
ok: true,
|
|
84
|
+
json: async () => ({ response: JSON.stringify(mockResponse) }),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const provider = new OllamaProvider(defaultConfig);
|
|
88
|
+
const result = await provider.analyzeSensors(sensorData);
|
|
89
|
+
|
|
90
|
+
expect(result.severity).toBe('critical');
|
|
91
|
+
expect(result.issue).toBe('High temperature detected');
|
|
92
|
+
expect(result.model).toBe('qwen3:14b');
|
|
93
|
+
expect(result.provider).toBe('ollama');
|
|
94
|
+
expect(result.response_time).toBeGreaterThanOrEqual(0);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should throw error on API failure', async () => {
|
|
98
|
+
mockFetch.mockResolvedValueOnce({
|
|
99
|
+
ok: false,
|
|
100
|
+
status: 500,
|
|
101
|
+
text: async () => 'Internal Server Error',
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const provider = new OllamaProvider(defaultConfig);
|
|
105
|
+
|
|
106
|
+
await expect(provider.analyzeSensors(sensorData)).rejects.toThrow('Ollama request failed');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should throw error on empty response', async () => {
|
|
110
|
+
mockFetch.mockResolvedValueOnce({
|
|
111
|
+
ok: true,
|
|
112
|
+
json: async () => ({ response: '' }),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const provider = new OllamaProvider(defaultConfig);
|
|
116
|
+
|
|
117
|
+
await expect(provider.analyzeSensors(sensorData)).rejects.toThrow('Empty response from ollama');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should throw error when no JSON in response', async () => {
|
|
121
|
+
mockFetch.mockResolvedValueOnce({
|
|
122
|
+
ok: true,
|
|
123
|
+
json: async () => ({ response: 'This is not JSON' }),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const provider = new OllamaProvider(defaultConfig);
|
|
127
|
+
|
|
128
|
+
await expect(provider.analyzeSensors(sensorData)).rejects.toThrow('No JSON found in AI response');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should extract JSON from response with extra text', async () => {
|
|
132
|
+
const mockResponse = {
|
|
133
|
+
severity: 'medium',
|
|
134
|
+
issue: 'Minor issue',
|
|
135
|
+
reasoning: 'Test',
|
|
136
|
+
recommended_actions: [],
|
|
137
|
+
ssh_diagnostics: [],
|
|
138
|
+
confidence: 0.8,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
mockFetch.mockResolvedValueOnce({
|
|
142
|
+
ok: true,
|
|
143
|
+
json: async () => ({
|
|
144
|
+
response: `Here is the analysis:\n${JSON.stringify(mockResponse)}\n\nEnd of analysis.`,
|
|
145
|
+
}),
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const provider = new OllamaProvider(defaultConfig);
|
|
149
|
+
const result = await provider.analyzeSensors(sensorData);
|
|
150
|
+
|
|
151
|
+
expect(result.severity).toBe('medium');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should include auth headers when API key is set', async () => {
|
|
155
|
+
const configWithKey: AIProviderConfig = {
|
|
156
|
+
...defaultConfig,
|
|
157
|
+
apiKey: 'test-api-key',
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const mockResponse = {
|
|
161
|
+
severity: 'low',
|
|
162
|
+
issue: 'Test',
|
|
163
|
+
reasoning: 'Test',
|
|
164
|
+
recommended_actions: [],
|
|
165
|
+
ssh_diagnostics: [],
|
|
166
|
+
confidence: 0.9,
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
mockFetch.mockResolvedValueOnce({
|
|
170
|
+
ok: true,
|
|
171
|
+
json: async () => ({ response: JSON.stringify(mockResponse) }),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const provider = new OllamaProvider(configWithKey);
|
|
175
|
+
await provider.analyzeSensors(sensorData);
|
|
176
|
+
|
|
177
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
178
|
+
expect.any(String),
|
|
179
|
+
expect.objectContaining({
|
|
180
|
+
headers: expect.objectContaining({
|
|
181
|
+
Authorization: 'Bearer test-api-key',
|
|
182
|
+
}),
|
|
183
|
+
})
|
|
184
|
+
);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
});
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { OpenAICompatibleProvider } from '../../src/localAI/providers/openai.js';
|
|
3
|
+
import type { AIProviderConfig, SensorData } from '../../src/localAI/types.js';
|
|
4
|
+
|
|
5
|
+
// Mock fetch globally
|
|
6
|
+
const mockFetch = vi.fn();
|
|
7
|
+
global.fetch = mockFetch;
|
|
8
|
+
|
|
9
|
+
describe('OpenAICompatibleProvider', () => {
|
|
10
|
+
const defaultConfig: AIProviderConfig = {
|
|
11
|
+
provider: 'openai',
|
|
12
|
+
baseUrl: 'http://localhost:8080',
|
|
13
|
+
model: 'gpt-3.5-turbo',
|
|
14
|
+
timeout: 60000,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
vi.clearAllMocks();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('getProviderName', () => {
|
|
22
|
+
it('should return "openai-compatible"', () => {
|
|
23
|
+
const provider = new OpenAICompatibleProvider(defaultConfig);
|
|
24
|
+
expect(provider.getProviderName()).toBe('openai-compatible');
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('checkHealth', () => {
|
|
29
|
+
it('should return true when API is available', async () => {
|
|
30
|
+
mockFetch.mockResolvedValueOnce({ ok: true });
|
|
31
|
+
|
|
32
|
+
const provider = new OpenAICompatibleProvider(defaultConfig);
|
|
33
|
+
const result = await provider.checkHealth();
|
|
34
|
+
|
|
35
|
+
expect(result).toBe(true);
|
|
36
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
37
|
+
'http://localhost:8080/v1/models',
|
|
38
|
+
expect.objectContaining({
|
|
39
|
+
signal: expect.any(AbortSignal),
|
|
40
|
+
})
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should return false when API is not available', async () => {
|
|
45
|
+
mockFetch.mockRejectedValueOnce(new Error('Connection refused'));
|
|
46
|
+
|
|
47
|
+
const provider = new OpenAICompatibleProvider(defaultConfig);
|
|
48
|
+
const result = await provider.checkHealth();
|
|
49
|
+
|
|
50
|
+
expect(result).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should return false when API returns non-ok response', async () => {
|
|
54
|
+
mockFetch.mockResolvedValueOnce({ ok: false });
|
|
55
|
+
|
|
56
|
+
const provider = new OpenAICompatibleProvider(defaultConfig);
|
|
57
|
+
const result = await provider.checkHealth();
|
|
58
|
+
|
|
59
|
+
expect(result).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should include auth headers in health check when API key is set', async () => {
|
|
63
|
+
const configWithKey: AIProviderConfig = {
|
|
64
|
+
...defaultConfig,
|
|
65
|
+
apiKey: 'test-api-key',
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
mockFetch.mockResolvedValueOnce({ ok: true });
|
|
69
|
+
|
|
70
|
+
const provider = new OpenAICompatibleProvider(configWithKey);
|
|
71
|
+
await provider.checkHealth();
|
|
72
|
+
|
|
73
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
74
|
+
expect.any(String),
|
|
75
|
+
expect.objectContaining({
|
|
76
|
+
headers: expect.objectContaining({
|
|
77
|
+
Authorization: 'Bearer test-api-key',
|
|
78
|
+
}),
|
|
79
|
+
})
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('analyzeSensors', () => {
|
|
85
|
+
const sensorData: SensorData = {
|
|
86
|
+
'sensor.temperature': {
|
|
87
|
+
state: '45',
|
|
88
|
+
unit: '°C',
|
|
89
|
+
attributes: { friendly_name: 'Indoor Temperature' },
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
it('should analyze sensors successfully', async () => {
|
|
94
|
+
const mockResponse = {
|
|
95
|
+
severity: 'critical',
|
|
96
|
+
issue: 'High temperature detected',
|
|
97
|
+
reasoning: 'Temperature is above 40°C',
|
|
98
|
+
recommended_actions: ['Check HVAC', 'Open windows'],
|
|
99
|
+
ssh_diagnostics: ['systemctl status hvac'],
|
|
100
|
+
confidence: 0.95,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
mockFetch.mockResolvedValueOnce({
|
|
104
|
+
ok: true,
|
|
105
|
+
json: async () => ({
|
|
106
|
+
choices: [
|
|
107
|
+
{
|
|
108
|
+
message: {
|
|
109
|
+
content: JSON.stringify(mockResponse),
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
}),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const provider = new OpenAICompatibleProvider(defaultConfig);
|
|
117
|
+
const result = await provider.analyzeSensors(sensorData);
|
|
118
|
+
|
|
119
|
+
expect(result.severity).toBe('critical');
|
|
120
|
+
expect(result.issue).toBe('High temperature detected');
|
|
121
|
+
expect(result.model).toBe('gpt-3.5-turbo');
|
|
122
|
+
expect(result.provider).toBe('openai-compatible');
|
|
123
|
+
expect(result.response_time).toBeGreaterThanOrEqual(0);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should use chat completions endpoint', async () => {
|
|
127
|
+
const mockResponse = {
|
|
128
|
+
severity: 'low',
|
|
129
|
+
issue: 'Test',
|
|
130
|
+
reasoning: 'Test',
|
|
131
|
+
recommended_actions: [],
|
|
132
|
+
ssh_diagnostics: [],
|
|
133
|
+
confidence: 0.9,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
mockFetch.mockResolvedValueOnce({
|
|
137
|
+
ok: true,
|
|
138
|
+
json: async () => ({
|
|
139
|
+
choices: [{ message: { content: JSON.stringify(mockResponse) } }],
|
|
140
|
+
}),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const provider = new OpenAICompatibleProvider(defaultConfig);
|
|
144
|
+
await provider.analyzeSensors(sensorData);
|
|
145
|
+
|
|
146
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
147
|
+
'http://localhost:8080/v1/chat/completions',
|
|
148
|
+
expect.objectContaining({
|
|
149
|
+
method: 'POST',
|
|
150
|
+
body: expect.stringContaining('"messages"'),
|
|
151
|
+
})
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should send messages in OpenAI format', async () => {
|
|
156
|
+
const mockResponse = {
|
|
157
|
+
severity: 'low',
|
|
158
|
+
issue: 'Test',
|
|
159
|
+
reasoning: 'Test',
|
|
160
|
+
recommended_actions: [],
|
|
161
|
+
ssh_diagnostics: [],
|
|
162
|
+
confidence: 0.9,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
mockFetch.mockResolvedValueOnce({
|
|
166
|
+
ok: true,
|
|
167
|
+
json: async () => ({
|
|
168
|
+
choices: [{ message: { content: JSON.stringify(mockResponse) } }],
|
|
169
|
+
}),
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const provider = new OpenAICompatibleProvider(defaultConfig);
|
|
173
|
+
await provider.analyzeSensors(sensorData);
|
|
174
|
+
|
|
175
|
+
const callBody = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
176
|
+
expect(callBody.messages).toHaveLength(2);
|
|
177
|
+
expect(callBody.messages[0].role).toBe('system');
|
|
178
|
+
expect(callBody.messages[1].role).toBe('user');
|
|
179
|
+
expect(callBody.model).toBe('gpt-3.5-turbo');
|
|
180
|
+
expect(callBody.temperature).toBe(0.3);
|
|
181
|
+
expect(callBody.max_tokens).toBe(2000);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should throw error on API failure', async () => {
|
|
185
|
+
mockFetch.mockResolvedValueOnce({
|
|
186
|
+
ok: false,
|
|
187
|
+
status: 500,
|
|
188
|
+
text: async () => 'Internal Server Error',
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const provider = new OpenAICompatibleProvider(defaultConfig);
|
|
192
|
+
|
|
193
|
+
await expect(provider.analyzeSensors(sensorData)).rejects.toThrow('OpenAI-compatible API request failed');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should throw error on API error response', async () => {
|
|
197
|
+
mockFetch.mockResolvedValueOnce({
|
|
198
|
+
ok: true,
|
|
199
|
+
json: async () => ({
|
|
200
|
+
error: { message: 'Model not found' },
|
|
201
|
+
}),
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const provider = new OpenAICompatibleProvider(defaultConfig);
|
|
205
|
+
|
|
206
|
+
await expect(provider.analyzeSensors(sensorData)).rejects.toThrow('API error: Model not found');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should throw error on empty response', async () => {
|
|
210
|
+
mockFetch.mockResolvedValueOnce({
|
|
211
|
+
ok: true,
|
|
212
|
+
json: async () => ({
|
|
213
|
+
choices: [{ message: { content: '' } }],
|
|
214
|
+
}),
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const provider = new OpenAICompatibleProvider(defaultConfig);
|
|
218
|
+
|
|
219
|
+
await expect(provider.analyzeSensors(sensorData)).rejects.toThrow('Empty response from openai-compatible');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should include auth headers when API key is set', async () => {
|
|
223
|
+
const configWithKey: AIProviderConfig = {
|
|
224
|
+
...defaultConfig,
|
|
225
|
+
apiKey: 'sk-test-key',
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const mockResponse = {
|
|
229
|
+
severity: 'low',
|
|
230
|
+
issue: 'Test',
|
|
231
|
+
reasoning: 'Test',
|
|
232
|
+
recommended_actions: [],
|
|
233
|
+
ssh_diagnostics: [],
|
|
234
|
+
confidence: 0.9,
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
mockFetch.mockResolvedValueOnce({
|
|
238
|
+
ok: true,
|
|
239
|
+
json: async () => ({
|
|
240
|
+
choices: [{ message: { content: JSON.stringify(mockResponse) } }],
|
|
241
|
+
}),
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const provider = new OpenAICompatibleProvider(configWithKey);
|
|
245
|
+
await provider.analyzeSensors(sensorData);
|
|
246
|
+
|
|
247
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
248
|
+
expect.any(String),
|
|
249
|
+
expect.objectContaining({
|
|
250
|
+
headers: expect.objectContaining({
|
|
251
|
+
Authorization: 'Bearer sk-test-key',
|
|
252
|
+
}),
|
|
253
|
+
})
|
|
254
|
+
);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server common functionality tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
6
|
+
import { createServer } from '../../src/server/common.js';
|
|
7
|
+
import { HaClient } from '../../src/haClient/index.js';
|
|
8
|
+
import type { EnvironmentConfig } from '../../src/config.js';
|
|
9
|
+
|
|
10
|
+
// Mock the tools registry
|
|
11
|
+
vi.mock('../../src/tools/registry.js', () => ({
|
|
12
|
+
registerAllTools: vi.fn(),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
describe('createServer', () => {
|
|
16
|
+
let mockClient: HaClient;
|
|
17
|
+
let mockConfig: EnvironmentConfig;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
mockConfig = {
|
|
21
|
+
baseUrl: 'http://homeassistant.10.0.0.19.nip.io:8123',
|
|
22
|
+
token: 'test-token',
|
|
23
|
+
strictSsl: false,
|
|
24
|
+
timeout: 30000,
|
|
25
|
+
aiProvider: 'ollama',
|
|
26
|
+
aiUrl: 'http://ollama.10.0.0.17.nip.io:11434',
|
|
27
|
+
aiModel: 'qwen3:14b',
|
|
28
|
+
aiTimeout: 60000,
|
|
29
|
+
logLevel: 'info',
|
|
30
|
+
logFormat: 'plain',
|
|
31
|
+
useHttp: false,
|
|
32
|
+
stateful: false,
|
|
33
|
+
httpTransport: 'stream',
|
|
34
|
+
httpEnableHealthcheck: true,
|
|
35
|
+
httpAllowCors: true,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
mockClient = new HaClient(mockConfig);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should create MCP server with correct name and version', () => {
|
|
42
|
+
const server = createServer(mockClient);
|
|
43
|
+
|
|
44
|
+
expect(server).toBeDefined();
|
|
45
|
+
// Server should have the correct configuration
|
|
46
|
+
// Note: We can't easily test internal properties, but we can verify it was created
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should register all tools on server creation', async () => {
|
|
50
|
+
const { registerAllTools } = await import('../../src/tools/registry.js');
|
|
51
|
+
const server = createServer(mockClient);
|
|
52
|
+
|
|
53
|
+
// Third argument (ollamaClient) is optional
|
|
54
|
+
expect(registerAllTools).toHaveBeenCalledWith(server, mockClient, undefined);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should create server with tools capability', () => {
|
|
58
|
+
const server = createServer(mockClient);
|
|
59
|
+
|
|
60
|
+
// The server should be created successfully
|
|
61
|
+
expect(server).toBeDefined();
|
|
62
|
+
expect(typeof server.connect).toBe('function');
|
|
63
|
+
});
|
|
64
|
+
});
|