@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.
- package/.env.example +1 -1
- package/README.md +11 -2
- package/dist/omadaClient/client.d.ts +6 -1
- package/dist/omadaClient/client.js +12 -1
- package/dist/omadaClient/client.js.map +1 -1
- package/dist/omadaClient/diagnose.d.ts +68 -0
- package/dist/omadaClient/diagnose.js +164 -0
- package/dist/omadaClient/diagnose.js.map +1 -0
- package/dist/omadaClient/index.d.ts +6 -0
- package/dist/omadaClient/index.js +15 -0
- package/dist/omadaClient/index.js.map +1 -1
- package/dist/omadaClient/macGroups.d.ts +58 -0
- package/dist/omadaClient/macGroups.js +124 -0
- package/dist/omadaClient/macGroups.js.map +1 -0
- package/dist/tools/infra/actions.d.ts +1 -1
- package/dist/tools/infra/actions.js +1 -1
- package/dist/tools/omada/index.js +6 -0
- package/dist/tools/omada/index.js.map +1 -1
- package/dist/tools/omada/macGroups.d.ts +19 -0
- package/dist/tools/omada/macGroups.js +77 -0
- package/dist/tools/omada/macGroups.js.map +1 -0
- package/dist/tools/omada/namespace.js +8 -0
- package/dist/tools/omada/namespace.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -1
- package/.codex/review-rules.md +0 -16
- package/.dockerignore +0 -8
- package/.gitattributes +0 -30
- package/.github/workflows/ci.yml +0 -193
- package/AGENTS.md +0 -389
- package/Dockerfile +0 -50
- package/docker-compose.yml +0 -23
- package/docs/CHANGELOG.md +0 -341
- package/docs/COVERAGE.md +0 -219
- package/docs/QUICK_START.md +0 -240
- package/eslint.config.js +0 -33
- package/n8n/ai-agent-safety-monitor.json +0 -355
- package/openapi.json +0 -1259
- package/scripts/check-version-sync.mjs +0 -69
- package/scripts/generate-jwt.ts +0 -121
- package/src/config.ts +0 -440
- package/src/haClient/automations.ts +0 -261
- package/src/haClient/calendars.ts +0 -94
- package/src/haClient/config.ts +0 -52
- package/src/haClient/devices.ts +0 -653
- package/src/haClient/entities.ts +0 -128
- package/src/haClient/events.ts +0 -689
- package/src/haClient/history.ts +0 -73
- package/src/haClient/index.ts +0 -336
- package/src/haClient/request.ts +0 -142
- package/src/haClient/services.ts +0 -45
- package/src/haClient/states.ts +0 -60
- package/src/haClient/updates.ts +0 -58
- package/src/haClient/wsCommand.ts +0 -44
- package/src/index.ts +0 -164
- package/src/localAI/index.ts +0 -88
- package/src/localAI/prompts.ts +0 -50
- package/src/localAI/providers/base.ts +0 -120
- package/src/localAI/providers/ollama.ts +0 -78
- package/src/localAI/providers/openai.ts +0 -86
- package/src/localAI/types.ts +0 -67
- package/src/omadaClient/auth.ts +0 -178
- package/src/omadaClient/client.ts +0 -337
- package/src/omadaClient/device.ts +0 -118
- package/src/omadaClient/index.ts +0 -310
- package/src/omadaClient/network.ts +0 -284
- package/src/omadaClient/request.ts +0 -365
- package/src/omadaClient/security.ts +0 -51
- package/src/omadaClient/site.ts +0 -344
- package/src/ownerQuestions/service.ts +0 -536
- package/src/permissions/index.ts +0 -143
- package/src/resources/index.ts +0 -213
- package/src/server/auth.ts +0 -175
- package/src/server/chatFace.ts +0 -789
- package/src/server/chatSlice.ts +0 -57
- package/src/server/common.ts +0 -170
- package/src/server/eventSubscription.ts +0 -343
- package/src/server/http.ts +0 -626
- package/src/server/instructions.ts +0 -97
- package/src/server/rateLimiter.ts +0 -227
- package/src/server/stdio.ts +0 -41
- package/src/server/stream.ts +0 -205
- package/src/tools/ai/analyzeSensors.ts +0 -33
- package/src/tools/ai/index.ts +0 -20
- package/src/tools/common.ts +0 -442
- package/src/tools/homeassistant/askOwner.ts +0 -133
- package/src/tools/homeassistant/automationControls.ts +0 -66
- package/src/tools/homeassistant/calendar.ts +0 -97
- package/src/tools/homeassistant/callService.ts +0 -25
- package/src/tools/homeassistant/controlClimate.ts +0 -68
- package/src/tools/homeassistant/controlCover.ts +0 -48
- package/src/tools/homeassistant/controlFan.ts +0 -44
- package/src/tools/homeassistant/controlLight.ts +0 -38
- package/src/tools/homeassistant/controlMediaPlayer.ts +0 -50
- package/src/tools/homeassistant/createAutomation.ts +0 -35
- package/src/tools/homeassistant/deleteAutomation.ts +0 -24
- package/src/tools/homeassistant/entityAction.ts +0 -35
- package/src/tools/homeassistant/entityGuard.ts +0 -28
- package/src/tools/homeassistant/getAllSensors.ts +0 -46
- package/src/tools/homeassistant/getAutomationTrace.ts +0 -30
- package/src/tools/homeassistant/getDomainSummary.ts +0 -29
- package/src/tools/homeassistant/getEntitiesByDomain.ts +0 -56
- package/src/tools/homeassistant/getHistory.ts +0 -24
- package/src/tools/homeassistant/getState.ts +0 -24
- package/src/tools/homeassistant/getStates.ts +0 -46
- package/src/tools/homeassistant/getVersion.ts +0 -21
- package/src/tools/homeassistant/index.ts +0 -121
- package/src/tools/homeassistant/listAutomations.ts +0 -35
- package/src/tools/homeassistant/listEntities.ts +0 -44
- package/src/tools/homeassistant/listPersons.ts +0 -46
- package/src/tools/homeassistant/sceneAndScript.ts +0 -53
- package/src/tools/homeassistant/searchEntities.ts +0 -58
- package/src/tools/homeassistant/sendNotification.ts +0 -179
- package/src/tools/homeassistant/systemTools.ts +0 -52
- package/src/tools/homeassistant/triggerAutomation.ts +0 -24
- package/src/tools/index.ts +0 -78
- package/src/tools/infra/actions.ts +0 -95
- package/src/tools/infra/index.ts +0 -138
- package/src/tools/omada/blockClient.ts +0 -28
- package/src/tools/omada/cyclePoePort.ts +0 -32
- package/src/tools/omada/deleteClient.ts +0 -27
- package/src/tools/omada/disableClientRateLimit.ts +0 -23
- package/src/tools/omada/getClient.ts +0 -14
- package/src/tools/omada/getDevice.ts +0 -14
- package/src/tools/omada/getFirewallSetting.ts +0 -14
- package/src/tools/omada/getInternetInfo.ts +0 -14
- package/src/tools/omada/getLanNetworkList.ts +0 -14
- package/src/tools/omada/getLanProfileList.ts +0 -14
- package/src/tools/omada/getPortForwardingStatus.ts +0 -26
- package/src/tools/omada/getRateLimitProfiles.ts +0 -14
- package/src/tools/omada/getSiteNtpStatus.ts +0 -14
- package/src/tools/omada/getSsidDetail.ts +0 -22
- package/src/tools/omada/getSsidList.ts +0 -21
- package/src/tools/omada/getSwitchStackDetail.ts +0 -14
- package/src/tools/omada/getThreatList.ts +0 -42
- package/src/tools/omada/getWlanGroupList.ts +0 -14
- package/src/tools/omada/graph.ts +0 -187
- package/src/tools/omada/index.ts +0 -153
- package/src/tools/omada/listBlockedClients.ts +0 -17
- package/src/tools/omada/listClients.ts +0 -14
- package/src/tools/omada/listClientsActivity.ts +0 -25
- package/src/tools/omada/listClientsPastConnections.ts +0 -44
- package/src/tools/omada/listDevices.ts +0 -14
- package/src/tools/omada/listDevicesStats.ts +0 -41
- package/src/tools/omada/listMostActiveClients.ts +0 -14
- package/src/tools/omada/listSites.ts +0 -18
- package/src/tools/omada/namespace.ts +0 -1164
- package/src/tools/omada/searchDevices.ts +0 -20
- package/src/tools/omada/setClientRateLimit.ts +0 -26
- package/src/tools/omada/setClientRateLimitProfile.ts +0 -25
- package/src/tools/omada/setSiteNtpServers.ts +0 -36
- package/src/tools/omada/setSsidEnabled.ts +0 -38
- package/src/tools/omada/unblockClient.ts +0 -26
- package/src/tools/types.ts +0 -11
- package/src/tools/utils/pagination-schema.ts +0 -27
- package/src/types/ai/index.ts +0 -11
- package/src/types/ai/types.ts +0 -67
- package/src/types/homeassistant/automations.ts +0 -43
- package/src/types/homeassistant/calendars.ts +0 -26
- package/src/types/homeassistant/config.ts +0 -14
- package/src/types/homeassistant/entities.ts +0 -14
- package/src/types/homeassistant/errors.ts +0 -33
- package/src/types/homeassistant/haConfig.ts +0 -7
- package/src/types/homeassistant/history.ts +0 -13
- package/src/types/homeassistant/index.ts +0 -46
- package/src/types/homeassistant/request.ts +0 -6
- package/src/types/homeassistant/serviceCall.ts +0 -18
- package/src/types/homeassistant/states.ts +0 -58
- package/src/types/homeassistant/updates.ts +0 -28
- package/src/types/index.ts +0 -12
- package/src/types/omada/activeClientInfo.ts +0 -17
- package/src/types/omada/clientActivity.ts +0 -32
- package/src/types/omada/clientBlockStatus.ts +0 -11
- package/src/types/omada/clientPastConnection.ts +0 -82
- package/src/types/omada/index.ts +0 -14
- package/src/types/omada/omadaApiResponse.ts +0 -5
- package/src/types/omada/omadaClientInfo.ts +0 -11
- package/src/types/omada/omadaDeviceInfo.ts +0 -11
- package/src/types/omada/omadaDeviceStats.ts +0 -31
- package/src/types/omada/omadaSiteSummary.ts +0 -5
- package/src/types/omada/oswStackDetail.ts +0 -11
- package/src/types/omada/paginatedResult.ts +0 -6
- package/src/types/omada/rateLimitProfile.ts +0 -58
- package/src/types/omada/threatInfo.ts +0 -38
- package/src/types/omada/tokenResult.ts +0 -6
- package/src/types.ts +0 -2
- package/src/utils/config-validations.ts +0 -68
- package/src/utils/jwt.ts +0 -112
- package/src/utils/logger.ts +0 -121
- package/src/utils/plainText.ts +0 -48
- package/src/utils/redact.ts +0 -59
- package/src/utils/sanitizeError.ts +0 -18
- package/src/utils/tlsDispatcher.ts +0 -69
- package/src/version.ts +0 -5
- package/tests/bdd/automation-creation.spec.ts +0 -214
- package/tests/bdd/automation-management.spec.ts +0 -267
- package/tests/bdd/calendar.spec.ts +0 -190
- package/tests/bdd/configuration.spec.ts +0 -283
- package/tests/bdd/device-control.spec.ts +0 -344
- package/tests/bdd/entity-listing.spec.ts +0 -208
- package/tests/bdd/entity-search.spec.ts +0 -183
- package/tests/bdd/history-retrieval.spec.ts +0 -195
- package/tests/bdd/notifications.spec.ts +0 -238
- package/tests/bdd/service-calls.spec.ts +0 -144
- package/tests/bdd/state-retrieval.spec.ts +0 -204
- package/tests/bench/hotpaths.bench.ts +0 -68
- package/tests/config.test.ts +0 -355
- package/tests/features/automation-creation.feature +0 -36
- package/tests/features/automation-management.feature +0 -54
- package/tests/features/calendar.feature +0 -37
- package/tests/features/configuration.feature +0 -70
- package/tests/features/device-control.feature +0 -100
- package/tests/features/entity-listing.feature +0 -51
- package/tests/features/entity-search.feature +0 -44
- package/tests/features/history-retrieval.feature +0 -38
- package/tests/features/notifications.feature +0 -37
- package/tests/features/service-calls.feature +0 -35
- package/tests/features/state-retrieval.feature +0 -37
- package/tests/fixtures/openapi-default-slice.json +0 -290
- package/tests/haClient/automationTrace.test.ts +0 -59
- package/tests/haClient/automations.test.ts +0 -149
- package/tests/haClient/config.test.ts +0 -117
- package/tests/haClient/entities.test.ts +0 -188
- package/tests/haClient/events.test.ts +0 -455
- package/tests/haClient/eventsTls.test.ts +0 -52
- package/tests/haClient/history.test.ts +0 -228
- package/tests/haClient/index.test.ts +0 -243
- package/tests/haClient/request.test.ts +0 -217
- package/tests/haClient/services.test.ts +0 -120
- package/tests/haClient/states.test.ts +0 -119
- package/tests/haClient/updates.test.ts +0 -193
- package/tests/index.test.ts +0 -160
- package/tests/integration/homeassistant.test.ts +0 -144
- package/tests/integration/omada.test.ts +0 -167
- package/tests/integration/server.test.ts +0 -151
- package/tests/localAI/index.test.ts +0 -177
- package/tests/localAI/ollama.test.ts +0 -187
- package/tests/localAI/openai.test.ts +0 -257
- package/tests/omadaClient/actions.test.ts +0 -161
- package/tests/omadaClient/clientBlocks.test.ts +0 -170
- package/tests/omadaClient/ntp.test.ts +0 -381
- package/tests/omadaClient/site.test.ts +0 -37
- package/tests/ownerQuestions/service.test.ts +0 -499
- package/tests/ownerQuestions/tools.test.ts +0 -186
- package/tests/permissions/index.test.ts +0 -217
- package/tests/resources/index.test.ts +0 -288
- package/tests/scripts/generateJwt.test.ts +0 -44
- package/tests/security/accessControl.test.ts +0 -223
- package/tests/security/authentication.test.ts +0 -117
- package/tests/security/configuration.test.ts +0 -170
- package/tests/security/cryptoTransport.test.ts +0 -172
- package/tests/security/dependencies.test.ts +0 -152
- package/tests/security/injection.test.ts +0 -117
- package/tests/security/insecureDesign.test.ts +0 -121
- package/tests/security/logging.test.ts +0 -131
- package/tests/security/resourceConsumption.test.ts +0 -142
- package/tests/server/auth.test.ts +0 -249
- package/tests/server/chatFace.test.ts +0 -320
- package/tests/server/common.test.ts +0 -84
- package/tests/server/http.test.ts +0 -561
- package/tests/server/instructions.test.ts +0 -152
- package/tests/server/memoizeJsonSchema.test.ts +0 -71
- package/tests/server/stdio.test.ts +0 -99
- package/tests/server/stream.test.ts +0 -285
- package/tests/server/toolVisibility.http.test.ts +0 -111
- package/tests/server/toolVisibility.test.ts +0 -129
- package/tests/tools/ai/analyzeSensors.test.ts +0 -227
- package/tests/tools/common.test.ts +0 -123
- package/tests/tools/homeassistant/handlers.test.ts +0 -872
- package/tests/tools/infra/invokeAction.test.ts +0 -280
- package/tests/tools/omada/graph.test.ts +0 -385
- package/tests/tools/omada/handlers.test.ts +0 -897
- package/tests/tools/omada/namespace.test.ts +0 -483
- package/tests/tools/registry.test.ts +0 -168
- package/tests/utils/config-validations.test.ts +0 -352
- package/tests/utils/jwt.test.ts +0 -115
- package/tests/utils/logger.test.ts +0 -327
- package/tests/utils/redact.test.ts +0 -105
- package/tsconfig.json +0 -24
- 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
|
-
});
|