@coffeerunhobby/mcp-ha-connect 2.2.1 → 2.3.1

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.
Files changed (281) hide show
  1. package/.env.example +1 -1
  2. package/README.md +11 -2
  3. package/dist/omadaClient/client.d.ts +6 -1
  4. package/dist/omadaClient/client.js +12 -1
  5. package/dist/omadaClient/client.js.map +1 -1
  6. package/dist/omadaClient/diagnose.d.ts +68 -0
  7. package/dist/omadaClient/diagnose.js +164 -0
  8. package/dist/omadaClient/diagnose.js.map +1 -0
  9. package/dist/omadaClient/index.d.ts +6 -0
  10. package/dist/omadaClient/index.js +15 -0
  11. package/dist/omadaClient/index.js.map +1 -1
  12. package/dist/omadaClient/macGroups.d.ts +58 -0
  13. package/dist/omadaClient/macGroups.js +124 -0
  14. package/dist/omadaClient/macGroups.js.map +1 -0
  15. package/dist/tools/infra/actions.d.ts +1 -1
  16. package/dist/tools/infra/actions.js +1 -1
  17. package/dist/tools/omada/index.js +6 -0
  18. package/dist/tools/omada/index.js.map +1 -1
  19. package/dist/tools/omada/macGroups.d.ts +19 -0
  20. package/dist/tools/omada/macGroups.js +77 -0
  21. package/dist/tools/omada/macGroups.js.map +1 -0
  22. package/dist/tools/omada/namespace.js +8 -0
  23. package/dist/tools/omada/namespace.js.map +1 -1
  24. package/dist/version.d.ts +1 -1
  25. package/dist/version.js +1 -1
  26. package/package.json +5 -1
  27. package/.codex/review-rules.md +0 -16
  28. package/.dockerignore +0 -8
  29. package/.gitattributes +0 -30
  30. package/.github/workflows/ci.yml +0 -193
  31. package/AGENTS.md +0 -389
  32. package/Dockerfile +0 -50
  33. package/docker-compose.yml +0 -23
  34. package/docs/CHANGELOG.md +0 -341
  35. package/docs/COVERAGE.md +0 -219
  36. package/docs/QUICK_START.md +0 -240
  37. package/eslint.config.js +0 -33
  38. package/n8n/ai-agent-safety-monitor.json +0 -355
  39. package/openapi.json +0 -1259
  40. package/scripts/check-version-sync.mjs +0 -69
  41. package/scripts/generate-jwt.ts +0 -121
  42. package/src/config.ts +0 -440
  43. package/src/haClient/automations.ts +0 -261
  44. package/src/haClient/calendars.ts +0 -94
  45. package/src/haClient/config.ts +0 -52
  46. package/src/haClient/devices.ts +0 -653
  47. package/src/haClient/entities.ts +0 -128
  48. package/src/haClient/events.ts +0 -689
  49. package/src/haClient/history.ts +0 -73
  50. package/src/haClient/index.ts +0 -336
  51. package/src/haClient/request.ts +0 -142
  52. package/src/haClient/services.ts +0 -45
  53. package/src/haClient/states.ts +0 -60
  54. package/src/haClient/updates.ts +0 -58
  55. package/src/haClient/wsCommand.ts +0 -44
  56. package/src/index.ts +0 -164
  57. package/src/localAI/index.ts +0 -88
  58. package/src/localAI/prompts.ts +0 -50
  59. package/src/localAI/providers/base.ts +0 -120
  60. package/src/localAI/providers/ollama.ts +0 -78
  61. package/src/localAI/providers/openai.ts +0 -86
  62. package/src/localAI/types.ts +0 -67
  63. package/src/omadaClient/auth.ts +0 -178
  64. package/src/omadaClient/client.ts +0 -337
  65. package/src/omadaClient/device.ts +0 -118
  66. package/src/omadaClient/index.ts +0 -310
  67. package/src/omadaClient/network.ts +0 -284
  68. package/src/omadaClient/request.ts +0 -365
  69. package/src/omadaClient/security.ts +0 -51
  70. package/src/omadaClient/site.ts +0 -344
  71. package/src/ownerQuestions/service.ts +0 -536
  72. package/src/permissions/index.ts +0 -143
  73. package/src/resources/index.ts +0 -213
  74. package/src/server/auth.ts +0 -175
  75. package/src/server/chatFace.ts +0 -789
  76. package/src/server/chatSlice.ts +0 -57
  77. package/src/server/common.ts +0 -170
  78. package/src/server/eventSubscription.ts +0 -343
  79. package/src/server/http.ts +0 -626
  80. package/src/server/instructions.ts +0 -97
  81. package/src/server/rateLimiter.ts +0 -227
  82. package/src/server/stdio.ts +0 -41
  83. package/src/server/stream.ts +0 -205
  84. package/src/tools/ai/analyzeSensors.ts +0 -33
  85. package/src/tools/ai/index.ts +0 -20
  86. package/src/tools/common.ts +0 -442
  87. package/src/tools/homeassistant/askOwner.ts +0 -133
  88. package/src/tools/homeassistant/automationControls.ts +0 -66
  89. package/src/tools/homeassistant/calendar.ts +0 -97
  90. package/src/tools/homeassistant/callService.ts +0 -25
  91. package/src/tools/homeassistant/controlClimate.ts +0 -68
  92. package/src/tools/homeassistant/controlCover.ts +0 -48
  93. package/src/tools/homeassistant/controlFan.ts +0 -44
  94. package/src/tools/homeassistant/controlLight.ts +0 -38
  95. package/src/tools/homeassistant/controlMediaPlayer.ts +0 -50
  96. package/src/tools/homeassistant/createAutomation.ts +0 -35
  97. package/src/tools/homeassistant/deleteAutomation.ts +0 -24
  98. package/src/tools/homeassistant/entityAction.ts +0 -35
  99. package/src/tools/homeassistant/entityGuard.ts +0 -28
  100. package/src/tools/homeassistant/getAllSensors.ts +0 -46
  101. package/src/tools/homeassistant/getAutomationTrace.ts +0 -30
  102. package/src/tools/homeassistant/getDomainSummary.ts +0 -29
  103. package/src/tools/homeassistant/getEntitiesByDomain.ts +0 -56
  104. package/src/tools/homeassistant/getHistory.ts +0 -24
  105. package/src/tools/homeassistant/getState.ts +0 -24
  106. package/src/tools/homeassistant/getStates.ts +0 -46
  107. package/src/tools/homeassistant/getVersion.ts +0 -21
  108. package/src/tools/homeassistant/index.ts +0 -121
  109. package/src/tools/homeassistant/listAutomations.ts +0 -35
  110. package/src/tools/homeassistant/listEntities.ts +0 -44
  111. package/src/tools/homeassistant/listPersons.ts +0 -46
  112. package/src/tools/homeassistant/sceneAndScript.ts +0 -53
  113. package/src/tools/homeassistant/searchEntities.ts +0 -58
  114. package/src/tools/homeassistant/sendNotification.ts +0 -179
  115. package/src/tools/homeassistant/systemTools.ts +0 -52
  116. package/src/tools/homeassistant/triggerAutomation.ts +0 -24
  117. package/src/tools/index.ts +0 -78
  118. package/src/tools/infra/actions.ts +0 -95
  119. package/src/tools/infra/index.ts +0 -138
  120. package/src/tools/omada/blockClient.ts +0 -28
  121. package/src/tools/omada/cyclePoePort.ts +0 -32
  122. package/src/tools/omada/deleteClient.ts +0 -27
  123. package/src/tools/omada/disableClientRateLimit.ts +0 -23
  124. package/src/tools/omada/getClient.ts +0 -14
  125. package/src/tools/omada/getDevice.ts +0 -14
  126. package/src/tools/omada/getFirewallSetting.ts +0 -14
  127. package/src/tools/omada/getInternetInfo.ts +0 -14
  128. package/src/tools/omada/getLanNetworkList.ts +0 -14
  129. package/src/tools/omada/getLanProfileList.ts +0 -14
  130. package/src/tools/omada/getPortForwardingStatus.ts +0 -26
  131. package/src/tools/omada/getRateLimitProfiles.ts +0 -14
  132. package/src/tools/omada/getSiteNtpStatus.ts +0 -14
  133. package/src/tools/omada/getSsidDetail.ts +0 -22
  134. package/src/tools/omada/getSsidList.ts +0 -21
  135. package/src/tools/omada/getSwitchStackDetail.ts +0 -14
  136. package/src/tools/omada/getThreatList.ts +0 -42
  137. package/src/tools/omada/getWlanGroupList.ts +0 -14
  138. package/src/tools/omada/graph.ts +0 -187
  139. package/src/tools/omada/index.ts +0 -153
  140. package/src/tools/omada/listBlockedClients.ts +0 -17
  141. package/src/tools/omada/listClients.ts +0 -14
  142. package/src/tools/omada/listClientsActivity.ts +0 -25
  143. package/src/tools/omada/listClientsPastConnections.ts +0 -44
  144. package/src/tools/omada/listDevices.ts +0 -14
  145. package/src/tools/omada/listDevicesStats.ts +0 -41
  146. package/src/tools/omada/listMostActiveClients.ts +0 -14
  147. package/src/tools/omada/listSites.ts +0 -18
  148. package/src/tools/omada/namespace.ts +0 -1164
  149. package/src/tools/omada/searchDevices.ts +0 -20
  150. package/src/tools/omada/setClientRateLimit.ts +0 -26
  151. package/src/tools/omada/setClientRateLimitProfile.ts +0 -25
  152. package/src/tools/omada/setSiteNtpServers.ts +0 -36
  153. package/src/tools/omada/setSsidEnabled.ts +0 -38
  154. package/src/tools/omada/unblockClient.ts +0 -26
  155. package/src/tools/types.ts +0 -11
  156. package/src/tools/utils/pagination-schema.ts +0 -27
  157. package/src/types/ai/index.ts +0 -11
  158. package/src/types/ai/types.ts +0 -67
  159. package/src/types/homeassistant/automations.ts +0 -43
  160. package/src/types/homeassistant/calendars.ts +0 -26
  161. package/src/types/homeassistant/config.ts +0 -14
  162. package/src/types/homeassistant/entities.ts +0 -14
  163. package/src/types/homeassistant/errors.ts +0 -33
  164. package/src/types/homeassistant/haConfig.ts +0 -7
  165. package/src/types/homeassistant/history.ts +0 -13
  166. package/src/types/homeassistant/index.ts +0 -46
  167. package/src/types/homeassistant/request.ts +0 -6
  168. package/src/types/homeassistant/serviceCall.ts +0 -18
  169. package/src/types/homeassistant/states.ts +0 -58
  170. package/src/types/homeassistant/updates.ts +0 -28
  171. package/src/types/index.ts +0 -12
  172. package/src/types/omada/activeClientInfo.ts +0 -17
  173. package/src/types/omada/clientActivity.ts +0 -32
  174. package/src/types/omada/clientBlockStatus.ts +0 -11
  175. package/src/types/omada/clientPastConnection.ts +0 -82
  176. package/src/types/omada/index.ts +0 -14
  177. package/src/types/omada/omadaApiResponse.ts +0 -5
  178. package/src/types/omada/omadaClientInfo.ts +0 -11
  179. package/src/types/omada/omadaDeviceInfo.ts +0 -11
  180. package/src/types/omada/omadaDeviceStats.ts +0 -31
  181. package/src/types/omada/omadaSiteSummary.ts +0 -5
  182. package/src/types/omada/oswStackDetail.ts +0 -11
  183. package/src/types/omada/paginatedResult.ts +0 -6
  184. package/src/types/omada/rateLimitProfile.ts +0 -58
  185. package/src/types/omada/threatInfo.ts +0 -38
  186. package/src/types/omada/tokenResult.ts +0 -6
  187. package/src/types.ts +0 -2
  188. package/src/utils/config-validations.ts +0 -68
  189. package/src/utils/jwt.ts +0 -112
  190. package/src/utils/logger.ts +0 -121
  191. package/src/utils/plainText.ts +0 -48
  192. package/src/utils/redact.ts +0 -59
  193. package/src/utils/sanitizeError.ts +0 -18
  194. package/src/utils/tlsDispatcher.ts +0 -69
  195. package/src/version.ts +0 -5
  196. package/tests/bdd/automation-creation.spec.ts +0 -214
  197. package/tests/bdd/automation-management.spec.ts +0 -267
  198. package/tests/bdd/calendar.spec.ts +0 -190
  199. package/tests/bdd/configuration.spec.ts +0 -283
  200. package/tests/bdd/device-control.spec.ts +0 -344
  201. package/tests/bdd/entity-listing.spec.ts +0 -208
  202. package/tests/bdd/entity-search.spec.ts +0 -183
  203. package/tests/bdd/history-retrieval.spec.ts +0 -195
  204. package/tests/bdd/notifications.spec.ts +0 -238
  205. package/tests/bdd/service-calls.spec.ts +0 -144
  206. package/tests/bdd/state-retrieval.spec.ts +0 -204
  207. package/tests/bench/hotpaths.bench.ts +0 -68
  208. package/tests/config.test.ts +0 -355
  209. package/tests/features/automation-creation.feature +0 -36
  210. package/tests/features/automation-management.feature +0 -54
  211. package/tests/features/calendar.feature +0 -37
  212. package/tests/features/configuration.feature +0 -70
  213. package/tests/features/device-control.feature +0 -100
  214. package/tests/features/entity-listing.feature +0 -51
  215. package/tests/features/entity-search.feature +0 -44
  216. package/tests/features/history-retrieval.feature +0 -38
  217. package/tests/features/notifications.feature +0 -37
  218. package/tests/features/service-calls.feature +0 -35
  219. package/tests/features/state-retrieval.feature +0 -37
  220. package/tests/fixtures/openapi-default-slice.json +0 -290
  221. package/tests/haClient/automationTrace.test.ts +0 -59
  222. package/tests/haClient/automations.test.ts +0 -149
  223. package/tests/haClient/config.test.ts +0 -117
  224. package/tests/haClient/entities.test.ts +0 -188
  225. package/tests/haClient/events.test.ts +0 -455
  226. package/tests/haClient/eventsTls.test.ts +0 -52
  227. package/tests/haClient/history.test.ts +0 -228
  228. package/tests/haClient/index.test.ts +0 -243
  229. package/tests/haClient/request.test.ts +0 -217
  230. package/tests/haClient/services.test.ts +0 -120
  231. package/tests/haClient/states.test.ts +0 -119
  232. package/tests/haClient/updates.test.ts +0 -193
  233. package/tests/index.test.ts +0 -160
  234. package/tests/integration/homeassistant.test.ts +0 -144
  235. package/tests/integration/omada.test.ts +0 -167
  236. package/tests/integration/server.test.ts +0 -151
  237. package/tests/localAI/index.test.ts +0 -177
  238. package/tests/localAI/ollama.test.ts +0 -187
  239. package/tests/localAI/openai.test.ts +0 -257
  240. package/tests/omadaClient/actions.test.ts +0 -161
  241. package/tests/omadaClient/clientBlocks.test.ts +0 -170
  242. package/tests/omadaClient/ntp.test.ts +0 -381
  243. package/tests/omadaClient/site.test.ts +0 -37
  244. package/tests/ownerQuestions/service.test.ts +0 -499
  245. package/tests/ownerQuestions/tools.test.ts +0 -186
  246. package/tests/permissions/index.test.ts +0 -217
  247. package/tests/resources/index.test.ts +0 -288
  248. package/tests/scripts/generateJwt.test.ts +0 -44
  249. package/tests/security/accessControl.test.ts +0 -223
  250. package/tests/security/authentication.test.ts +0 -117
  251. package/tests/security/configuration.test.ts +0 -170
  252. package/tests/security/cryptoTransport.test.ts +0 -172
  253. package/tests/security/dependencies.test.ts +0 -152
  254. package/tests/security/injection.test.ts +0 -117
  255. package/tests/security/insecureDesign.test.ts +0 -121
  256. package/tests/security/logging.test.ts +0 -131
  257. package/tests/security/resourceConsumption.test.ts +0 -142
  258. package/tests/server/auth.test.ts +0 -249
  259. package/tests/server/chatFace.test.ts +0 -320
  260. package/tests/server/common.test.ts +0 -84
  261. package/tests/server/http.test.ts +0 -561
  262. package/tests/server/instructions.test.ts +0 -152
  263. package/tests/server/memoizeJsonSchema.test.ts +0 -71
  264. package/tests/server/stdio.test.ts +0 -99
  265. package/tests/server/stream.test.ts +0 -285
  266. package/tests/server/toolVisibility.http.test.ts +0 -111
  267. package/tests/server/toolVisibility.test.ts +0 -129
  268. package/tests/tools/ai/analyzeSensors.test.ts +0 -227
  269. package/tests/tools/common.test.ts +0 -123
  270. package/tests/tools/homeassistant/handlers.test.ts +0 -872
  271. package/tests/tools/infra/invokeAction.test.ts +0 -280
  272. package/tests/tools/omada/graph.test.ts +0 -385
  273. package/tests/tools/omada/handlers.test.ts +0 -897
  274. package/tests/tools/omada/namespace.test.ts +0 -483
  275. package/tests/tools/registry.test.ts +0 -168
  276. package/tests/utils/config-validations.test.ts +0 -352
  277. package/tests/utils/jwt.test.ts +0 -115
  278. package/tests/utils/logger.test.ts +0 -327
  279. package/tests/utils/redact.test.ts +0 -105
  280. package/tsconfig.json +0 -24
  281. package/vitest.config.ts +0 -38
@@ -1,187 +0,0 @@
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
- });
@@ -1,257 +0,0 @@
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
- });
@@ -1,161 +0,0 @@
1
- /**
2
- * Unit tests for the v1.6 Omada action operations:
3
- * - DeviceOperations.cyclePoePorts (PoE recovery = remote hard-reboot of PoE devices)
4
- * - NetworkOperations.setSsidEnabled (SSID on/off via the WLAN-schedule mechanism)
5
- */
6
-
7
- import { describe, it, expect, vi, beforeEach } from 'vitest';
8
-
9
- import { DeviceOperations } from '../../src/omadaClient/device.js';
10
- import { NetworkOperations, ALWAYS_PROFILE_NAME } from '../../src/omadaClient/network.js';
11
- import type { RequestHandler } from '../../src/omadaClient/request.js';
12
- import type { SiteOperations } from '../../src/omadaClient/site.js';
13
-
14
- const buildPath = (p: string): string => `/openapi/v1/omadac1${p.startsWith('/') ? p : `/${p}`}`;
15
-
16
- function createMockRequest() {
17
- return {
18
- get: vi.fn(),
19
- post: vi.fn(),
20
- patch: vi.fn(),
21
- fetchPaginated: vi.fn(),
22
- // Mirrors the real ensureSuccess: unwrap result, throw on errorCode !== 0
23
- ensureSuccess: vi.fn((response: { errorCode: number; msg?: string; result?: unknown }) => {
24
- if (response.errorCode !== 0) {
25
- throw new Error(response.msg ?? `Omada error ${String(response.errorCode)}`);
26
- }
27
- return response.result;
28
- }),
29
- };
30
- }
31
-
32
- const mockSite = { resolveSiteId: vi.fn(() => 'site-1') } as unknown as SiteOperations;
33
-
34
- const ok = (result?: unknown) => ({ errorCode: 0, msg: 'Success', result });
35
-
36
- describe('DeviceOperations.cyclePoePorts', () => {
37
- let request: ReturnType<typeof createMockRequest>;
38
- let ops: DeviceOperations;
39
-
40
- beforeEach(() => {
41
- request = createMockRequest();
42
- ops = new DeviceOperations(request as unknown as RequestHandler, mockSite, buildPath);
43
- });
44
-
45
- it('POSTs the poe-recovery endpoint with the ports payload', async () => {
46
- request.post.mockResolvedValue(ok());
47
-
48
- await ops.cyclePoePorts('AA-BB-CC-DD-EE-FF', [1, 3]);
49
-
50
- expect(request.post).toHaveBeenCalledWith(
51
- '/openapi/v1/omadac1/sites/site-1/switches/AA-BB-CC-DD-EE-FF/multi-ports/poe-recovery',
52
- { ports: [1, 3] }
53
- );
54
- });
55
-
56
- it('rejects a missing switchMac with an actionable hint', async () => {
57
- await expect(ops.cyclePoePorts('', [1])).rejects.toThrow(/switchMac.*omada_listDevices/);
58
- expect(request.post).not.toHaveBeenCalled();
59
- });
60
-
61
- it('rejects an empty ports array', async () => {
62
- await expect(ops.cyclePoePorts('AA-BB-CC-DD-EE-FF', [])).rejects.toThrow(/at least one port/i);
63
- expect(request.post).not.toHaveBeenCalled();
64
- });
65
-
66
- it('propagates controller errors via ensureSuccess', async () => {
67
- request.post.mockResolvedValue({ errorCode: -1, msg: 'port not PoE capable' });
68
- await expect(ops.cyclePoePorts('AA-BB-CC-DD-EE-FF', [9])).rejects.toThrow('port not PoE capable');
69
- });
70
- });
71
-
72
- describe('NetworkOperations.setSsidEnabled', () => {
73
- let request: ReturnType<typeof createMockRequest>;
74
- let ops: NetworkOperations;
75
-
76
- const WLANS = [
77
- {
78
- wlanId: 'wlan-1',
79
- wlanName: 'Default',
80
- ssidList: [
81
- { ssidId: 'ssid-main', ssidName: 'Tower' },
82
- { ssidId: 'ssid-guest', ssidName: 'Tower-Guest' },
83
- ],
84
- },
85
- ];
86
-
87
- beforeEach(() => {
88
- request = createMockRequest();
89
- ops = new NetworkOperations(request as unknown as RequestHandler, mockSite, buildPath);
90
- });
91
-
92
- it('enables an SSID by simply disabling its WLAN schedule', async () => {
93
- request.get.mockResolvedValueOnce(ok(WLANS)); // listAllSsids
94
- request.patch.mockResolvedValue(ok());
95
-
96
- const result = await ops.setSsidEnabled('Tower-Guest', true);
97
-
98
- expect(request.patch).toHaveBeenCalledWith(
99
- '/openapi/v1/omadac1/sites/site-1/wireless-network/wlans/wlan-1/ssids/ssid-guest/update-wlan-schedule',
100
- { wlanScheduleEnable: false }
101
- );
102
- expect(result).toMatchObject({ ssidId: 'ssid-guest', ssidName: 'Tower-Guest', enabled: true });
103
- });
104
-
105
- it('resolves the SSID case-insensitively by name, or exactly by ssidId', async () => {
106
- request.get.mockResolvedValue(ok(WLANS));
107
- request.patch.mockResolvedValue(ok());
108
-
109
- await ops.setSsidEnabled('tower-guest', true);
110
- await ops.setSsidEnabled('ssid-main', true);
111
-
112
- expect(request.patch).toHaveBeenCalledTimes(2);
113
- expect(request.patch.mock.calls[1][0]).toContain('/ssids/ssid-main/');
114
- });
115
-
116
- it('disables an SSID by applying a radio-off schedule with the reusable 24/7 profile', async () => {
117
- request.get
118
- .mockResolvedValueOnce(ok(WLANS)) // listAllSsids
119
- .mockResolvedValueOnce(ok([{ profileId: 'prof-247', name: ALWAYS_PROFILE_NAME }])); // profiles: exists
120
- request.patch.mockResolvedValue(ok());
121
-
122
- const result = await ops.setSsidEnabled('Tower-Guest', false);
123
-
124
- expect(request.post).not.toHaveBeenCalled(); // profile existed — no create
125
- expect(request.patch).toHaveBeenCalledWith(
126
- expect.stringContaining('/ssids/ssid-guest/update-wlan-schedule'),
127
- { wlanScheduleEnable: true, action: 0, scheduleId: 'prof-247' }
128
- );
129
- expect(result).toMatchObject({ enabled: false, scheduleId: 'prof-247' });
130
- });
131
-
132
- it('creates the 24/7 profile once when missing (and re-lists for its id — the API returns none)', async () => {
133
- request.get
134
- .mockResolvedValueOnce(ok(WLANS)) // listAllSsids
135
- .mockResolvedValueOnce(ok([{ profileId: 'other', name: 'unrelated' }])) // profiles: missing
136
- .mockResolvedValueOnce(ok([{ profileId: 'prof-new', name: ALWAYS_PROFILE_NAME }])); // re-list after create
137
- request.post.mockResolvedValue(ok());
138
- request.patch.mockResolvedValue(ok());
139
-
140
- const result = await ops.setSsidEnabled('Tower-Guest', false);
141
-
142
- expect(request.post).toHaveBeenCalledWith(
143
- '/openapi/v1/omadac1/sites/site-1/time-range-profiles',
144
- expect.objectContaining({
145
- name: ALWAYS_PROFILE_NAME,
146
- dayMode: 0,
147
- timeList: [expect.objectContaining({ startTimeH: 0, endTimeH: 24 })],
148
- })
149
- );
150
- expect(result.scheduleId).toBe('prof-new');
151
- });
152
-
153
- it('rejects an unknown SSID and lists the available ones', async () => {
154
- request.get.mockResolvedValueOnce(ok(WLANS));
155
-
156
- await expect(ops.setSsidEnabled('DoesNotExist', false)).rejects.toThrow(
157
- /not found.*Tower.*Tower-Guest/s
158
- );
159
- expect(request.patch).not.toHaveBeenCalled();
160
- });
161
- });