@coffeerunhobby/mcp-ha-connect 2.2.1 → 2.4.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/.env.example +1 -1
- package/README.md +24 -2
- package/dist/omadaClient/accessControl.d.ts +290 -0
- package/dist/omadaClient/accessControl.js +509 -0
- package/dist/omadaClient/accessControl.js.map +1 -0
- 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 +9 -0
- package/dist/omadaClient/index.js +19 -0
- package/dist/omadaClient/index.js.map +1 -1
- package/dist/omadaClient/macGroups.d.ts +58 -0
- package/dist/omadaClient/macGroups.js +110 -0
- package/dist/omadaClient/macGroups.js.map +1 -0
- package/dist/omadaClient/serialize.d.ts +13 -0
- package/dist/omadaClient/serialize.js +29 -0
- package/dist/omadaClient/serialize.js.map +1 -0
- package/dist/omadaClient/timeRange.d.ts +41 -0
- package/dist/omadaClient/timeRange.js +109 -0
- package/dist/omadaClient/timeRange.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/accessControl.d.ts +151 -0
- package/dist/tools/omada/accessControl.js +158 -0
- package/dist/tools/omada/accessControl.js.map +1 -0
- package/dist/tools/omada/index.js +9 -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 +16 -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,483 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for the Omada resource-graph namespace manifest.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { describe, it, expect } from 'vitest';
|
|
6
|
-
import {
|
|
7
|
-
OMADA_RESOURCES,
|
|
8
|
-
auditFilters,
|
|
9
|
-
childrenOf,
|
|
10
|
-
getResourceNode,
|
|
11
|
-
normalizePath,
|
|
12
|
-
resolveLogWindowMs,
|
|
13
|
-
resolveUsageWindowSec,
|
|
14
|
-
type ReadArgs,
|
|
15
|
-
} from '../../../src/tools/omada/namespace.js';
|
|
16
|
-
import { Permission } from '../../../src/permissions/index.js';
|
|
17
|
-
|
|
18
|
-
/** A minimal OmadaClient stand-in that records the last readResource() call. */
|
|
19
|
-
function makeRecordingClient(): { client: any; calls: Array<Record<string, unknown>> } {
|
|
20
|
-
const calls: Array<Record<string, unknown>> = [];
|
|
21
|
-
const client = {
|
|
22
|
-
readResource: (opts: Record<string, unknown>) => {
|
|
23
|
-
calls.push(opts);
|
|
24
|
-
return Promise.resolve({ ok: true });
|
|
25
|
-
},
|
|
26
|
-
// Typed client methods backing a few graph nodes (recorded like readResource).
|
|
27
|
-
searchDevices: (searchKey: string) => {
|
|
28
|
-
calls.push({ __method: 'searchDevices', searchKey });
|
|
29
|
-
return Promise.resolve([]);
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
return { client, calls };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async function fetchOpts(path: string, args: ReadArgs = {}): Promise<Record<string, unknown>> {
|
|
36
|
-
const node = getResourceNode(path);
|
|
37
|
-
if (!node?.fetch) {
|
|
38
|
-
throw new Error(`no fetch for ${path}`);
|
|
39
|
-
}
|
|
40
|
-
const { client, calls } = makeRecordingClient();
|
|
41
|
-
await node.fetch(client, args);
|
|
42
|
-
return calls[0];
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
describe('Omada namespace - normalizePath', () => {
|
|
46
|
-
it('maps empty / root to "/"', () => {
|
|
47
|
-
expect(normalizePath('')).toBe('/');
|
|
48
|
-
expect(normalizePath('/')).toBe('/');
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('adds a leading slash', () => {
|
|
52
|
-
expect(normalizePath('gateway')).toBe('/gateway');
|
|
53
|
-
expect(normalizePath('gateway/wan')).toBe('/gateway/wan');
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('strips a trailing slash', () => {
|
|
57
|
-
expect(normalizePath('/gateway/')).toBe('/gateway');
|
|
58
|
-
expect(normalizePath('/clients/active/')).toBe('/clients/active');
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('trims whitespace', () => {
|
|
62
|
-
expect(normalizePath(' /gateway ')).toBe('/gateway');
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
describe('Omada namespace - getResourceNode', () => {
|
|
67
|
-
it('resolves a known node and normalizes input', () => {
|
|
68
|
-
expect(getResourceNode('/gateway/wan')?.path).toBe('/gateway/wan');
|
|
69
|
-
expect(getResourceNode('gateway/wan/')?.path).toBe('/gateway/wan');
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('returns undefined for unknown paths', () => {
|
|
73
|
-
expect(getResourceNode('/does/not/exist')).toBeUndefined();
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('resolves the root container', () => {
|
|
77
|
-
const root = getResourceNode('/');
|
|
78
|
-
expect(root?.kind).toBe('container');
|
|
79
|
-
expect(root?.fetch).toBeUndefined();
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
describe('Omada namespace - childrenOf', () => {
|
|
84
|
-
it('lists top-level children at root', () => {
|
|
85
|
-
const paths = childrenOf('/').map((n) => n.path);
|
|
86
|
-
expect(paths).toContain('/sites');
|
|
87
|
-
expect(paths).toContain('/devices');
|
|
88
|
-
expect(paths).toContain('/clients');
|
|
89
|
-
expect(paths).toContain('/gateway');
|
|
90
|
-
expect(paths).toContain('/network');
|
|
91
|
-
expect(paths).toContain('/wifi');
|
|
92
|
-
expect(paths).toContain('/events');
|
|
93
|
-
expect(paths).toContain('/dashboard');
|
|
94
|
-
expect(paths).toContain('/firmware');
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('does not include grandchildren at root', () => {
|
|
98
|
-
const paths = childrenOf('/').map((n) => n.path);
|
|
99
|
-
expect(paths).not.toContain('/gateway/wan');
|
|
100
|
-
expect(paths).not.toContain('/clients/active');
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('lists direct children of a container', () => {
|
|
104
|
-
const paths = childrenOf('/gateway').map((n) => n.path);
|
|
105
|
-
expect(paths).toEqual(expect.arrayContaining(['/gateway/wan', '/gateway/health']));
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('returns no children for a leaf', () => {
|
|
109
|
-
expect(childrenOf('/network/firewall')).toEqual([]);
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
describe('Omada namespace - manifest invariants', () => {
|
|
114
|
-
it('every node has a unique, normalized path', () => {
|
|
115
|
-
const seen = new Set<string>();
|
|
116
|
-
for (const node of OMADA_RESOURCES) {
|
|
117
|
-
expect(node.path).toBe(normalizePath(node.path));
|
|
118
|
-
expect(seen.has(node.path)).toBe(false);
|
|
119
|
-
seen.add(node.path);
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('containers are browse-only (no fetch); collections/leaves are readable', () => {
|
|
124
|
-
for (const node of OMADA_RESOURCES) {
|
|
125
|
-
if (node.kind === 'container') {
|
|
126
|
-
expect(node.fetch).toBeUndefined();
|
|
127
|
-
} else {
|
|
128
|
-
expect(typeof node.fetch).toBe('function');
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it('every non-root node has an existing parent in the manifest', () => {
|
|
134
|
-
const byPath = new Map(OMADA_RESOURCES.map((n) => [n.path, n]));
|
|
135
|
-
for (const node of OMADA_RESOURCES) {
|
|
136
|
-
if (node.path === '/') {
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
const idx = node.path.lastIndexOf('/');
|
|
140
|
-
const parent = idx <= 0 ? '/' : node.path.slice(0, idx);
|
|
141
|
-
expect(byPath.has(parent)).toBe(true);
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it('every readable node declares a non-zero permission bit (fail-closed)', () => {
|
|
146
|
-
for (const node of OMADA_RESOURCES) {
|
|
147
|
-
if (node.fetch) {
|
|
148
|
-
expect(node.permission).toBeGreaterThan(0);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('reads are QUERY except the ADMIN-gated security and audit subtrees', () => {
|
|
154
|
-
for (const node of OMADA_RESOURCES) {
|
|
155
|
-
if (!node.fetch) {
|
|
156
|
-
continue;
|
|
157
|
-
}
|
|
158
|
-
if (node.path.startsWith('/security') || node.path.startsWith('/audit')) {
|
|
159
|
-
expect(node.permission).toBe(Permission.ADMIN);
|
|
160
|
-
} else {
|
|
161
|
-
expect((node.permission & Permission.QUERY) === Permission.QUERY).toBe(true);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it('the /security and /audit containers are ADMIN-gated', () => {
|
|
167
|
-
expect(getResourceNode('/security')?.permission).toBe(Permission.ADMIN);
|
|
168
|
-
expect(getResourceNode('/audit')?.permission).toBe(Permission.ADMIN);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it('paginated nodes declare a default page size', () => {
|
|
172
|
-
for (const node of OMADA_RESOURCES) {
|
|
173
|
-
if (node.paginated) {
|
|
174
|
-
expect(typeof node.defaultPageSize).toBe('number');
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
describe('Omada namespace - time-window helpers', () => {
|
|
181
|
-
const NOW_MS = 1_700_000_000_000;
|
|
182
|
-
|
|
183
|
-
describe('resolveLogWindowMs (epoch ms, default 7 days)', () => {
|
|
184
|
-
it('defaults to the last 7 days when no params are given', () => {
|
|
185
|
-
const { timeStart, timeEnd } = resolveLogWindowMs(undefined, NOW_MS);
|
|
186
|
-
expect(timeEnd).toBe(NOW_MS);
|
|
187
|
-
expect(timeStart).toBe(NOW_MS - 7 * 24 * 60 * 60 * 1000);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it('honours explicit startTime/endTime params', () => {
|
|
191
|
-
const { timeStart, timeEnd } = resolveLogWindowMs({ startTime: '111', endTime: '222' }, NOW_MS);
|
|
192
|
-
expect(timeStart).toBe(111);
|
|
193
|
-
expect(timeEnd).toBe(222);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it('defaults only the missing bound', () => {
|
|
197
|
-
const { timeStart, timeEnd } = resolveLogWindowMs({ endTime: '500' }, NOW_MS);
|
|
198
|
-
expect(timeEnd).toBe(500);
|
|
199
|
-
expect(timeStart).toBe(500 - 7 * 24 * 60 * 60 * 1000);
|
|
200
|
-
});
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
describe('resolveUsageWindowSec (epoch seconds, default 24h)', () => {
|
|
204
|
-
it('defaults to the last 24h in seconds when no params are given', () => {
|
|
205
|
-
const { start, end } = resolveUsageWindowSec(undefined, NOW_MS);
|
|
206
|
-
expect(end).toBe(Math.floor(NOW_MS / 1000));
|
|
207
|
-
expect(start).toBe(Math.floor(NOW_MS / 1000) - 24 * 60 * 60);
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it('honours explicit startTime/endTime params (seconds)', () => {
|
|
211
|
-
const { start, end } = resolveUsageWindowSec({ startTime: '1000', endTime: '2000' }, NOW_MS);
|
|
212
|
-
expect(start).toBe(1000);
|
|
213
|
-
expect(end).toBe(2000);
|
|
214
|
-
});
|
|
215
|
-
});
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
describe('Omada namespace - fixed node wiring', () => {
|
|
219
|
-
it('/events forwards a default millisecond window as filters.timeStart/timeEnd', async () => {
|
|
220
|
-
const opts = await fetchOpts('/events');
|
|
221
|
-
expect(opts.pathTemplate).toBe('/sites/{siteId}/logs/events');
|
|
222
|
-
expect(opts.paginated).toBe(true);
|
|
223
|
-
const query = opts.query as Record<string, number>;
|
|
224
|
-
expect(query['filters.timeStart']).toBeTypeOf('number');
|
|
225
|
-
expect(query['filters.timeEnd']).toBeTypeOf('number');
|
|
226
|
-
expect(query['filters.timeEnd']).toBeGreaterThan(query['filters.timeStart']);
|
|
227
|
-
// 13-digit epoch-ms, not seconds.
|
|
228
|
-
expect(String(query['filters.timeEnd']).length).toBeGreaterThanOrEqual(13);
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
it('/events passes an optional module filter through', async () => {
|
|
232
|
-
const opts = await fetchOpts('/events', { params: { module: 'Device' } });
|
|
233
|
-
expect((opts.query as Record<string, unknown>)['filters.module']).toBe('Device');
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
it('/events/alerts forwards filters.resolved only for valid boolean strings', async () => {
|
|
237
|
-
const resolved = await fetchOpts('/events/alerts', { params: { resolved: 'true' } });
|
|
238
|
-
expect((resolved.query as Record<string, unknown>)['filters.resolved']).toBe('true');
|
|
239
|
-
const ignored = await fetchOpts('/events/alerts', { params: { resolved: 'maybe' } });
|
|
240
|
-
expect((ignored.query as Record<string, unknown>)['filters.resolved']).toBeUndefined();
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
it('/dashboard/cpu and /dashboard/memory forward a second-precision window', async () => {
|
|
244
|
-
for (const path of ['/dashboard/cpu', '/dashboard/memory']) {
|
|
245
|
-
const opts = await fetchOpts(path);
|
|
246
|
-
const query = opts.query as Record<string, number>;
|
|
247
|
-
expect(query.start).toBeTypeOf('number');
|
|
248
|
-
expect(query.end).toBeTypeOf('number');
|
|
249
|
-
expect(query.end).toBeGreaterThan(query.start);
|
|
250
|
-
// 10-digit epoch-seconds, not ms.
|
|
251
|
-
expect(String(query.end).length).toBeLessThanOrEqual(11);
|
|
252
|
-
}
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
it('/devices/pending, /wifi/rogue, /wifi/wids are paginated single-page reads', async () => {
|
|
256
|
-
for (const path of ['/devices/pending', '/wifi/rogue', '/wifi/wids']) {
|
|
257
|
-
const opts = await fetchOpts(path, { page: 2, pageSize: 25 });
|
|
258
|
-
expect(opts.paginated).toBe(true);
|
|
259
|
-
expect(opts.page).toBe(2);
|
|
260
|
-
expect(opts.pageSize).toBe(25);
|
|
261
|
-
}
|
|
262
|
-
});
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
describe('Omada namespace - Tier-2 / loose-end node wiring', () => {
|
|
266
|
-
it('/devices/search forwards searchKey to the typed client method', async () => {
|
|
267
|
-
const opts = await fetchOpts('/devices/search', { params: { searchKey: 'switch' } });
|
|
268
|
-
expect(opts.__method).toBe('searchDevices');
|
|
269
|
-
expect(opts.searchKey).toBe('switch');
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
it('/devices/cable-test is a browse-only container', () => {
|
|
273
|
-
const node = getResourceNode('/devices/cable-test');
|
|
274
|
-
expect(node?.kind).toBe('container');
|
|
275
|
-
expect(node?.fetch).toBeUndefined();
|
|
276
|
-
const children = childrenOf('/devices/cable-test').map((n) => n.path);
|
|
277
|
-
expect(children).toEqual(
|
|
278
|
-
expect.arrayContaining(['/devices/cable-test/ports', '/devices/cable-test/results', '/devices/cable-test/logs'])
|
|
279
|
-
);
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
it('cable-test nodes substitute switchMac into the path template', async () => {
|
|
283
|
-
const cases: Array<[string, string]> = [
|
|
284
|
-
['/devices/cable-test/ports', '/sites/{siteId}/cable-test/switches/{switchMac}/ports'],
|
|
285
|
-
['/devices/cable-test/results', '/sites/{siteId}/cable-test/switches/{switchMac}/full-results'],
|
|
286
|
-
['/devices/cable-test/logs', '/sites/{siteId}/cable-test/switches/{switchMac}/logs'],
|
|
287
|
-
];
|
|
288
|
-
for (const [path, tmpl] of cases) {
|
|
289
|
-
const opts = await fetchOpts(path, { params: { switchMac: 'AA-BB-CC-DD-EE-FF' } });
|
|
290
|
-
expect(opts.pathTemplate).toBe(tmpl);
|
|
291
|
-
expect((opts.pathParams as Record<string, string>).switchMac).toBe('AA-BB-CC-DD-EE-FF');
|
|
292
|
-
}
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
it('/network/port-forwarding/rules reads the NAT rule list, paginated', async () => {
|
|
296
|
-
const opts = await fetchOpts('/network/port-forwarding/rules', { page: 3, pageSize: 20 });
|
|
297
|
-
expect(opts.pathTemplate).toBe('/sites/{siteId}/nat/port-forwardings');
|
|
298
|
-
expect(opts.paginated).toBe(true);
|
|
299
|
-
expect(opts.page).toBe(3);
|
|
300
|
-
expect(opts.pageSize).toBe(20);
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
it('/network/dhcp-leases reads the DHCP user-list, paginated', async () => {
|
|
304
|
-
const opts = await fetchOpts('/network/dhcp-leases');
|
|
305
|
-
expect(opts.pathTemplate).toBe('/sites/{siteId}/setting/service/dhcp/user-list');
|
|
306
|
-
expect(opts.paginated).toBe(true);
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
it('/network/load-balance reads WAN load-balance status (no params)', async () => {
|
|
310
|
-
const opts = await fetchOpts('/network/load-balance');
|
|
311
|
-
expect(opts.pathTemplate).toBe('/sites/{siteId}/internet/load-balance/status');
|
|
312
|
-
expect(opts.paginated).toBeFalsy();
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
it('/dashboard/client-distribution reads with no time window', async () => {
|
|
316
|
-
const opts = await fetchOpts('/dashboard/client-distribution');
|
|
317
|
-
expect(opts.pathTemplate).toBe('/sites/{siteId}/dashboard/client-distribution');
|
|
318
|
-
expect(opts.query).toBeUndefined();
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
it('/dashboard/traffic-distribution and /dashboard/traffic-activities forward a second-precision window', async () => {
|
|
322
|
-
for (const path of ['/dashboard/traffic-distribution', '/dashboard/traffic-activities']) {
|
|
323
|
-
const opts = await fetchOpts(path);
|
|
324
|
-
const query = opts.query as Record<string, number>;
|
|
325
|
-
expect(query.start).toBeTypeOf('number');
|
|
326
|
-
expect(query.end).toBeTypeOf('number');
|
|
327
|
-
expect(query.end).toBeGreaterThan(query.start);
|
|
328
|
-
// 10-digit epoch-seconds, not ms.
|
|
329
|
-
expect(String(query.end).length).toBeLessThanOrEqual(11);
|
|
330
|
-
}
|
|
331
|
-
});
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
describe('Omada namespace - Tier-3 curated node wiring', () => {
|
|
335
|
-
// Each entry: graph path → expected controller path template (all paginated unless noted).
|
|
336
|
-
const PAGINATED: Array<[string, string]> = [
|
|
337
|
-
['/devices/poe', '/sites/{siteId}/switches/ports/poe-info'],
|
|
338
|
-
['/network/dhcp-reservations', '/sites/{siteId}/setting/service/dhcp'],
|
|
339
|
-
['/network/static-routes', '/sites/{siteId}/routing/static-routings'],
|
|
340
|
-
['/network/ip-mac-binding', '/sites/{siteId}/ip-mac-binds'],
|
|
341
|
-
['/network/acls/gateway', '/sites/{siteId}/acls/osg-acls'],
|
|
342
|
-
['/network/acls/switch', '/sites/{siteId}/acls/osw-acls'],
|
|
343
|
-
['/network/acls/eap', '/sites/{siteId}/acls/eap-acls'],
|
|
344
|
-
['/network/url-filters/gateway', '/sites/{siteId}/url-filters/gateway'],
|
|
345
|
-
['/network/url-filters/eap', '/sites/{siteId}/url-filters/eap'],
|
|
346
|
-
['/network/mac-filters/allow', '/sites/{siteId}/mac-filters/allow'],
|
|
347
|
-
['/network/mac-filters/deny', '/sites/{siteId}/mac-filters/deny'],
|
|
348
|
-
];
|
|
349
|
-
const SIMPLE: Array<[string, string]> = [
|
|
350
|
-
['/devices/lldp', '/sites/{siteId}/lldp'],
|
|
351
|
-
['/network/attack-defense', '/sites/{siteId}/attack-defense'],
|
|
352
|
-
['/wifi/band-steering', '/sites/{siteId}/band-steering'],
|
|
353
|
-
];
|
|
354
|
-
|
|
355
|
-
it('paginated Tier-3 nodes map to the correct controller path and forward page/pageSize', async () => {
|
|
356
|
-
for (const [path, tmpl] of PAGINATED) {
|
|
357
|
-
const opts = await fetchOpts(path, { page: 2, pageSize: 10 });
|
|
358
|
-
expect(opts.pathTemplate).toBe(tmpl);
|
|
359
|
-
expect(opts.paginated).toBe(true);
|
|
360
|
-
expect(opts.page).toBe(2);
|
|
361
|
-
expect(opts.pageSize).toBe(10);
|
|
362
|
-
}
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
it('simple Tier-3 nodes map to the correct controller path with no pagination', async () => {
|
|
366
|
-
for (const [path, tmpl] of SIMPLE) {
|
|
367
|
-
const opts = await fetchOpts(path);
|
|
368
|
-
expect(opts.pathTemplate).toBe(tmpl);
|
|
369
|
-
expect(opts.paginated).toBeFalsy();
|
|
370
|
-
}
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
it('Tier-3 grouping containers are browse-only with the expected children', () => {
|
|
374
|
-
const groups: Array<[string, string[]]> = [
|
|
375
|
-
['/network/acls', ['/network/acls/gateway', '/network/acls/switch', '/network/acls/eap']],
|
|
376
|
-
['/network/url-filters', ['/network/url-filters/gateway', '/network/url-filters/eap']],
|
|
377
|
-
['/network/mac-filters', ['/network/mac-filters/allow', '/network/mac-filters/deny']],
|
|
378
|
-
];
|
|
379
|
-
for (const [parent, kids] of groups) {
|
|
380
|
-
expect(getResourceNode(parent)?.kind).toBe('container');
|
|
381
|
-
expect(getResourceNode(parent)?.fetch).toBeUndefined();
|
|
382
|
-
expect(childrenOf(parent).map((n) => n.path)).toEqual(expect.arrayContaining(kids));
|
|
383
|
-
}
|
|
384
|
-
});
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
describe('Omada namespace - Tier-4 node wiring (VPN / profiles / schedules / backup / audit)', () => {
|
|
388
|
-
it('exposes the Tier-4 subtrees at the root', () => {
|
|
389
|
-
const paths = childrenOf('/').map((n) => n.path);
|
|
390
|
-
expect(paths).toEqual(expect.arrayContaining(['/vpn', '/profiles', '/schedules', '/backup', '/audit']));
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
it('Tier-4 grouping containers are browse-only with the expected children', () => {
|
|
394
|
-
const groups: Array<[string, string[]]> = [
|
|
395
|
-
['/vpn', ['/vpn/site-to-site', '/vpn/client-to-site', '/vpn/wireguard', '/vpn/ipsec-stats']],
|
|
396
|
-
['/vpn/client-to-site', ['/vpn/client-to-site/servers', '/vpn/client-to-site/clients']],
|
|
397
|
-
['/profiles', ['/profiles/ppsk', '/profiles/time-range']],
|
|
398
|
-
['/schedules', ['/schedules/poe', '/schedules/port', '/schedules/upgrade']],
|
|
399
|
-
['/backup', ['/backup/files', '/backup/result']],
|
|
400
|
-
['/audit', ['/audit/site', '/audit/global']],
|
|
401
|
-
];
|
|
402
|
-
for (const [parent, kids] of groups) {
|
|
403
|
-
expect(getResourceNode(parent)?.kind).toBe('container');
|
|
404
|
-
expect(getResourceNode(parent)?.fetch).toBeUndefined();
|
|
405
|
-
expect(childrenOf(parent).map((n) => n.path)).toEqual(expect.arrayContaining(kids));
|
|
406
|
-
}
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
it('simple Tier-4 reads map to the correct controller path with no pagination', async () => {
|
|
410
|
-
const SIMPLE: Array<[string, string]> = [
|
|
411
|
-
['/vpn/site-to-site', '/sites/{siteId}/vpn/site-to-site-vpns'],
|
|
412
|
-
['/vpn/client-to-site/servers', '/sites/{siteId}/vpn/client-to-site-vpn-servers'],
|
|
413
|
-
['/vpn/client-to-site/clients', '/sites/{siteId}/vpn/client-to-site-vpn-clients'],
|
|
414
|
-
['/vpn/wireguard', '/sites/{siteId}/vpn/wireguards'],
|
|
415
|
-
['/profiles/time-range', '/sites/{siteId}/time-range-profiles'],
|
|
416
|
-
['/schedules/poe', '/sites/{siteId}/poe-schedules'],
|
|
417
|
-
['/schedules/port', '/sites/{siteId}/port-schedules'],
|
|
418
|
-
['/schedules/upgrade', '/sites/{siteId}/upgrade-schedules'],
|
|
419
|
-
['/backup/files', '/sites/{siteId}/maintenance/backup/files'],
|
|
420
|
-
['/backup/result', '/sites/{siteId}/backup/result'],
|
|
421
|
-
];
|
|
422
|
-
for (const [path, tmpl] of SIMPLE) {
|
|
423
|
-
const opts = await fetchOpts(path);
|
|
424
|
-
expect(opts.pathTemplate).toBe(tmpl);
|
|
425
|
-
expect(opts.paginated).toBeFalsy();
|
|
426
|
-
}
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
it('/vpn/site-to-site fetches a single tunnel by id', async () => {
|
|
430
|
-
const opts = await fetchOpts('/vpn/site-to-site', { id: 'vpn-123' });
|
|
431
|
-
expect(opts.pathTemplate).toBe('/sites/{siteId}/vpn/site-to-site-vpns/{vpnId}');
|
|
432
|
-
expect((opts.pathParams as Record<string, string>).vpnId).toBe('vpn-123');
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
it('/vpn/ipsec-stats is a paginated single-page read', async () => {
|
|
436
|
-
const opts = await fetchOpts('/vpn/ipsec-stats', { page: 2, pageSize: 25 });
|
|
437
|
-
expect(opts.pathTemplate).toBe('/sites/{siteId}/setting/vpn/stats/ipsec');
|
|
438
|
-
expect(opts.paginated).toBe(true);
|
|
439
|
-
expect(opts.page).toBe(2);
|
|
440
|
-
expect(opts.pageSize).toBe(25);
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
it('/profiles/ppsk forwards the required type as a query param', async () => {
|
|
444
|
-
const opts = await fetchOpts('/profiles/ppsk', { params: { type: '1' } });
|
|
445
|
-
expect(opts.pathTemplate).toBe('/sites/{siteId}/ppsk-profiles');
|
|
446
|
-
expect((opts.query as Record<string, unknown>).type).toBe(1);
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
it('/audit/site is a site-scoped paginated ADMIN read', async () => {
|
|
450
|
-
const opts = await fetchOpts('/audit/site', { page: 3, pageSize: 20 });
|
|
451
|
-
expect(opts.pathTemplate).toBe('/sites/{siteId}/audit-logs');
|
|
452
|
-
expect(opts.paginated).toBe(true);
|
|
453
|
-
expect(opts.page).toBe(3);
|
|
454
|
-
expect(opts.pageSize).toBe(20);
|
|
455
|
-
// No filters supplied → empty query object.
|
|
456
|
-
expect(opts.query).toEqual({});
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
it('/audit/global is NOT site-scoped and reads the controller-wide log', async () => {
|
|
460
|
-
const opts = await fetchOpts('/audit/global');
|
|
461
|
-
expect(opts.pathTemplate).toBe('/audit-logs');
|
|
462
|
-
expect(opts.siteScoped).toBe(false);
|
|
463
|
-
expect(opts.paginated).toBe(true);
|
|
464
|
-
});
|
|
465
|
-
});
|
|
466
|
-
|
|
467
|
-
describe('Omada namespace - auditFilters helper', () => {
|
|
468
|
-
it('returns an empty object when no params are given', () => {
|
|
469
|
-
expect(auditFilters(undefined)).toEqual({});
|
|
470
|
-
expect(auditFilters({})).toEqual({});
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
it('maps startTime/endTime to numeric filters.* keys', () => {
|
|
474
|
-
expect(auditFilters({ startTime: '1700000000000', endTime: '1700003600000' })).toEqual({
|
|
475
|
-
'filters.startTime': 1700000000000,
|
|
476
|
-
'filters.endTime': 1700003600000,
|
|
477
|
-
});
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
it('passes searchKey through and omits absent bounds', () => {
|
|
481
|
-
expect(auditFilters({ searchKey: 'login' })).toEqual({ searchKey: 'login' });
|
|
482
|
-
});
|
|
483
|
-
});
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tool registration tests
|
|
3
|
-
* Tests the individual tool registration functions using McpServer
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
7
|
-
import { McpServer } from '@modelcontextprotocol/server';
|
|
8
|
-
import { registerAllTools } from '../../src/tools/index.js';
|
|
9
|
-
|
|
10
|
-
// Mock HaClient
|
|
11
|
-
const mockClient = {
|
|
12
|
-
getStates: vi.fn().mockResolvedValue([]),
|
|
13
|
-
getState: vi.fn().mockResolvedValue({}),
|
|
14
|
-
callService: vi.fn().mockResolvedValue({ context: {} }),
|
|
15
|
-
getEntitiesByDomain: vi.fn().mockResolvedValue([]),
|
|
16
|
-
searchEntities: vi.fn().mockResolvedValue([]),
|
|
17
|
-
getAllSensors: vi.fn().mockResolvedValue([]),
|
|
18
|
-
getDomainSummary: vi.fn().mockResolvedValue({}),
|
|
19
|
-
getHistory: vi.fn().mockResolvedValue([]),
|
|
20
|
-
getVersion: vi.fn().mockResolvedValue({ version: '2024.1.0' }),
|
|
21
|
-
getLogbook: vi.fn().mockResolvedValue([]),
|
|
22
|
-
listAutomations: vi.fn().mockResolvedValue([]),
|
|
23
|
-
triggerAutomation: vi.fn().mockResolvedValue({}),
|
|
24
|
-
createAutomation: vi.fn().mockResolvedValue({ id: 'test-id' }),
|
|
25
|
-
deleteAutomation: vi.fn().mockResolvedValue({}),
|
|
26
|
-
getAutomationTrace: vi.fn().mockResolvedValue({}),
|
|
27
|
-
checkUpdates: vi.fn().mockResolvedValue({ available: [] }),
|
|
28
|
-
devices: {
|
|
29
|
-
triggerAutomation: vi.fn().mockResolvedValue({}),
|
|
30
|
-
reloadAutomations: vi.fn().mockResolvedValue({}),
|
|
31
|
-
},
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
describe('Tool Registration', () => {
|
|
35
|
-
let server: McpServer;
|
|
36
|
-
|
|
37
|
-
beforeEach(() => {
|
|
38
|
-
vi.clearAllMocks();
|
|
39
|
-
|
|
40
|
-
server = new McpServer({
|
|
41
|
-
name: 'test-server',
|
|
42
|
-
version: '1.0.0',
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
describe('registerAllTools', () => {
|
|
47
|
-
it('should register all tools without errors', () => {
|
|
48
|
-
expect(() => registerAllTools({ server, haClient: mockClient as any })).not.toThrow();
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('should register tools on McpServer', () => {
|
|
52
|
-
registerAllTools({ server, haClient: mockClient as any });
|
|
53
|
-
|
|
54
|
-
// The server should have handlers registered
|
|
55
|
-
expect(server).toBeDefined();
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('should work with optional aiClient', () => {
|
|
59
|
-
expect(() => registerAllTools({ server, haClient: mockClient as any, aiClient: undefined })).not.toThrow();
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('should work without haClient (Omada-only mode)', () => {
|
|
63
|
-
expect(() => registerAllTools({ server })).not.toThrow();
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
describe('Tool Categories', () => {
|
|
68
|
-
it('should include core entity tools', () => {
|
|
69
|
-
const coreTools = [
|
|
70
|
-
'getStates',
|
|
71
|
-
'getState',
|
|
72
|
-
'callService',
|
|
73
|
-
'getEntitiesByDomain',
|
|
74
|
-
'searchEntities',
|
|
75
|
-
'getAllSensors',
|
|
76
|
-
'listEntities',
|
|
77
|
-
'getDomainSummary',
|
|
78
|
-
];
|
|
79
|
-
|
|
80
|
-
expect(coreTools.length).toBeGreaterThan(5);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('should include automation tools', () => {
|
|
84
|
-
const automationTools = [
|
|
85
|
-
'listAutomations',
|
|
86
|
-
'triggerAutomation',
|
|
87
|
-
'enableAutomation',
|
|
88
|
-
'disableAutomation',
|
|
89
|
-
'toggleAutomation',
|
|
90
|
-
'reloadAutomations',
|
|
91
|
-
'createAutomation',
|
|
92
|
-
'deleteAutomation',
|
|
93
|
-
'getAutomationTrace',
|
|
94
|
-
];
|
|
95
|
-
|
|
96
|
-
expect(automationTools).toContain('listAutomations');
|
|
97
|
-
expect(automationTools).toContain('createAutomation');
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('should include device control tools', () => {
|
|
101
|
-
const deviceTools = [
|
|
102
|
-
'controlLight',
|
|
103
|
-
'controlClimate',
|
|
104
|
-
'controlMediaPlayer',
|
|
105
|
-
'controlCover',
|
|
106
|
-
'controlFan',
|
|
107
|
-
];
|
|
108
|
-
|
|
109
|
-
expect(deviceTools).toContain('controlLight');
|
|
110
|
-
expect(deviceTools).toContain('controlClimate');
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('should include utility tools', () => {
|
|
114
|
-
const utilityTools = [
|
|
115
|
-
'getHistory',
|
|
116
|
-
'getVersion',
|
|
117
|
-
'entityAction',
|
|
118
|
-
'activateScene',
|
|
119
|
-
'runScript',
|
|
120
|
-
'sendNotification',
|
|
121
|
-
];
|
|
122
|
-
|
|
123
|
-
expect(utilityTools).toContain('getHistory');
|
|
124
|
-
expect(utilityTools).toContain('getVersion');
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('should include system tools', () => {
|
|
128
|
-
const systemTools = [
|
|
129
|
-
'restartHomeAssistant',
|
|
130
|
-
'getSystemLog',
|
|
131
|
-
'checkUpdates',
|
|
132
|
-
];
|
|
133
|
-
|
|
134
|
-
expect(systemTools).toContain('restartHomeAssistant');
|
|
135
|
-
expect(systemTools).toContain('checkUpdates');
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
describe('Tool Response Format', () => {
|
|
140
|
-
it('should return text content type', () => {
|
|
141
|
-
const response = {
|
|
142
|
-
content: [
|
|
143
|
-
{
|
|
144
|
-
type: 'text',
|
|
145
|
-
text: JSON.stringify({ success: true }),
|
|
146
|
-
},
|
|
147
|
-
],
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
expect(response.content[0].type).toBe('text');
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('should return valid JSON in text field', () => {
|
|
154
|
-
const response = {
|
|
155
|
-
content: [
|
|
156
|
-
{
|
|
157
|
-
type: 'text',
|
|
158
|
-
text: JSON.stringify({ entity_id: 'light.living_room', state: 'on' }),
|
|
159
|
-
},
|
|
160
|
-
],
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
expect(() => JSON.parse(response.content[0].text)).not.toThrow();
|
|
164
|
-
const parsed = JSON.parse(response.content[0].text);
|
|
165
|
-
expect(parsed.entity_id).toBe('light.living_room');
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
});
|