Skip to content

Commit d7d564f

Browse files
feat: comprehensive tests (20 cases), README with full device fields
Tests cover: - MCP conformance (10 tools, schemas, annotations) - Counter type classification (gauge vs counter vs unknown) - bareCounterName extraction from full PerfMon paths - Counter presets validation (no UnregisteredPhoneCount on CUCM 15) - computeStats with actual timestamps, delta/rate, empty samples README updated with full 24-field device_status output from live CUCM 15 SOAP audit (model, product, httpd, registrationAttempts, isCtiControllable, linesStatus, downloadStatus, etc.)
1 parent b2e1ec7 commit d7d564f

2 files changed

Lines changed: 129 additions & 1 deletion

File tree

README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ Query device registration by name pattern with full per-device detail.
142142
{ "query": "SEP*" }
143143
```
144144

145-
**Output:**
145+
**Output (all fields from CUCM SOAP response, strongly typed):**
146146

147147
```json
148148
{
@@ -155,12 +155,29 @@ Query device registration by name pattern with full per-device detail.
155155
{
156156
"name": "SEP0022905C7710",
157157
"ipAddress": "10.0.0.178",
158+
"ipAddrType": "ipv4",
159+
"ipAttribute": "AdministrativeAndSignaling",
158160
"description": "Auto 1000 7975 Phone3",
159161
"dirNumber": "1000-Registered",
160162
"status": "Registered",
161163
"statusReason": 0,
162164
"protocol": "SCCP",
165+
"deviceClass": "Phone",
166+
"model": 437,
167+
"product": 336,
168+
"httpd": "Yes",
169+
"registrationAttempts": 1,
170+
"isCtiControllable": true,
171+
"loginUserId": "",
172+
"numOfLines": 1,
173+
"linesStatus": [
174+
{ "directoryNumber": "1000", "status": "Registered" }
175+
],
163176
"activeLoadId": "SCCP75.9-4-2SR4-3S",
177+
"inactiveLoadId": "",
178+
"downloadStatus": "Unknown",
179+
"downloadFailureReason": "",
180+
"downloadServer": "",
164181
"timeStamp": 1773835197
165182
}
166183
]

test/mcp-conformance.test.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { describe, it, expect } from "vitest";
22
import { getTools } from "../src/tools/index.js";
3+
import { computeStats, classifyCounter, bareCounterName, COUNTER_PRESETS } from "../src/types/perfmon-types.js";
4+
import type { TimestampedSample } from "../src/types/perfmon-types.js";
35

46
describe("MCP Conformance", () => {
57
const tools = getTools();
@@ -45,3 +47,112 @@ describe("MCP Conformance", () => {
4547
expect(names.has("registration_health")).toBe(true);
4648
});
4749
});
50+
51+
describe("Counter type classification", () => {
52+
it("classifies gauge counters", () => {
53+
expect(classifyCounter("CallsActive")).toBe("gauge");
54+
expect(classifyCounter("RegisteredHardwarePhones")).toBe("gauge");
55+
expect(classifyCounter("PartiallyRegisteredPhone")).toBe("gauge");
56+
});
57+
58+
it("classifies monotonic counters", () => {
59+
expect(classifyCounter("CallsCompleted")).toBe("counter");
60+
expect(classifyCounter("CallsAttempted")).toBe("counter");
61+
});
62+
63+
it("classifies unknown counters", () => {
64+
expect(classifyCounter("SomethingCustom")).toBe("unknown");
65+
});
66+
67+
it("classifies counters from full PerfMon paths", () => {
68+
expect(classifyCounter("\\\\192.168.1.1\\Cisco CallManager\\CallsActive")).toBe("gauge");
69+
expect(classifyCounter("\\\\192.168.1.1\\Cisco CallManager\\CallsCompleted")).toBe("counter");
70+
});
71+
});
72+
73+
describe("bareCounterName", () => {
74+
it("extracts bare name from full path", () => {
75+
expect(bareCounterName("\\\\192.168.1.1\\Cisco CallManager\\CallsActive")).toBe("CallsActive");
76+
});
77+
78+
it("returns bare name unchanged", () => {
79+
expect(bareCounterName("CallsActive")).toBe("CallsActive");
80+
});
81+
82+
it("handles empty string", () => {
83+
expect(bareCounterName("")).toBe("");
84+
});
85+
});
86+
87+
describe("Counter presets", () => {
88+
it("registration preset uses valid counter names", () => {
89+
const preset = COUNTER_PRESETS.registration;
90+
expect(preset).toBeDefined();
91+
expect(preset!.object).toBe("Cisco CallManager");
92+
expect(preset!.counters).toContain("RegisteredHardwarePhones");
93+
expect(preset!.counters).toContain("RegisteredOtherStationDevices");
94+
expect(preset!.counters).toContain("PartiallyRegisteredPhone");
95+
// UnregisteredPhoneCount does NOT exist on CUCM 15
96+
expect(preset!.counters).not.toContain("UnregisteredPhoneCount");
97+
});
98+
99+
it("call_processing preset has expected counters", () => {
100+
const preset = COUNTER_PRESETS.call_processing;
101+
expect(preset!.counters).toContain("CallsActive");
102+
expect(preset!.counters).toContain("CallsAttempted");
103+
expect(preset!.counters).toContain("CallsCompleted");
104+
});
105+
106+
it("all presets have an object name", () => {
107+
for (const [name, preset] of Object.entries(COUNTER_PRESETS)) {
108+
expect(preset.object, `preset ${name} missing object`).toBeTruthy();
109+
}
110+
});
111+
});
112+
113+
describe("computeStats", () => {
114+
const samples: TimestampedSample[] = [
115+
{ timestamp: 1000, counters: [{ name: "CallsActive", value: 5, cStatus: 1 }, { name: "CallsCompleted", value: 100, cStatus: 1 }] },
116+
{ timestamp: 6000, counters: [{ name: "CallsActive", value: 8, cStatus: 1 }, { name: "CallsCompleted", value: 103, cStatus: 1 }] },
117+
{ timestamp: 11000, counters: [{ name: "CallsActive", value: 3, cStatus: 1 }, { name: "CallsCompleted", value: 108, cStatus: 1 }] },
118+
];
119+
120+
it("computes min/max/avg for gauge counters", () => {
121+
const stats = computeStats(samples, "CallsActive");
122+
expect(stats.type).toBe("gauge");
123+
expect(stats.min).toBe(3);
124+
expect(stats.max).toBe(8);
125+
expect(stats.avg).toBeCloseTo(5.33, 1);
126+
expect(stats.latest).toBe(3);
127+
});
128+
129+
it("computes delta/rate for monotonic counters", () => {
130+
const stats = computeStats(samples, "CallsCompleted");
131+
expect(stats.type).toBe("counter");
132+
expect(stats.delta).toBe(8); // 108 - 100
133+
expect(stats.rate).toBeCloseTo(0.8, 1); // 8 / 10 seconds
134+
expect(stats.latest).toBe(108);
135+
});
136+
137+
it("uses actual timestamps for rate calculation", () => {
138+
const stats = computeStats(samples, "CallsCompleted");
139+
// Duration is 11000 - 1000 = 10000ms = 10s
140+
// Delta is 8, so rate = 8/10 = 0.8 per second
141+
expect(stats.rate).toBeCloseTo(0.8, 2);
142+
});
143+
144+
it("handles empty samples", () => {
145+
const stats = computeStats([], "CallsActive");
146+
expect(stats.values).toHaveLength(0);
147+
expect(stats.min).toBe(0);
148+
expect(stats.max).toBe(0);
149+
expect(stats.avg).toBe(0);
150+
expect(stats.rate).toBe(0);
151+
});
152+
153+
it("handles counter not found in samples", () => {
154+
const stats = computeStats(samples, "NonExistentCounter");
155+
expect(stats.type).toBe("unknown");
156+
expect(stats.values).toHaveLength(0);
157+
});
158+
});

0 commit comments

Comments
 (0)