@access-mcp/system-status
Version:
MCP server for ACCESS-CI System Status and Outages API
381 lines (380 loc) • 17.6 kB
JavaScript
import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
import { SystemStatusServer } from "../server.js";
// Mock axios
vi.mock("axios");
describe("SystemStatusServer", () => {
let server;
let mockHttpClient;
const mockCurrentOutagesData = [
{
id: "1",
Subject: "Emergency maintenance on Anvil",
Content: "Critical issue requiring immediate attention",
CreationTime: "2024-08-27T10:00:00Z",
LastModificationTime: "2024-08-27T11:00:00Z",
AffectedResources: [
{ ResourceName: "Anvil", ResourceID: "anvil-1" }
]
},
{
id: "2",
Subject: "Scheduled maintenance on Bridges-2",
Content: "Regular maintenance window",
CreationTime: "2024-08-27T08:00:00Z",
LastModificationTime: "2024-08-27T08:30:00Z",
AffectedResources: [
{ ResourceName: "Bridges-2", ResourceID: "bridges2-1" }
]
}
];
const mockFutureOutagesData = [
{
id: "3",
Subject: "Scheduled Jetstream maintenance",
Content: "Planned maintenance",
CreationTime: "2024-08-27T09:00:00Z",
LastModificationTime: "2024-08-27T09:00:00Z",
OutageStartDateTime: "2024-08-30T10:00:00Z",
OutageEndDateTime: "2024-08-30T14:00:00Z",
AffectedResources: [
{ ResourceName: "Jetstream", ResourceID: "jetstream-1" }
]
}
];
const mockPastOutagesData = [
{
id: "4",
Subject: "Past maintenance on Stampede3",
Content: "Completed maintenance",
CreationTime: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(), // 5 days ago
LastModificationTime: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), // 2 days ago
OutageStartDateTime: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(), // 3 days ago
OutageEndDateTime: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000 + 6 * 60 * 60 * 1000).toISOString(), // 3 days ago + 6 hours
OutageType: "Full",
AffectedResources: [
{ ResourceName: "Stampede3", ResourceID: "stampede3-1" }
]
}
];
beforeEach(() => {
server = new SystemStatusServer();
// Set up mock HTTP client
mockHttpClient = {
get: vi.fn(),
};
// Override the httpClient getter
Object.defineProperty(server, "httpClient", {
get: () => mockHttpClient,
configurable: true,
});
});
afterEach(() => {
vi.clearAllMocks();
});
describe("Server Initialization", () => {
it("should initialize with correct server info", () => {
expect(server).toBeDefined();
expect(server["serverName"]).toBe("access-mcp-system-status");
expect(server["version"]).toBe("0.4.0");
expect(server["baseURL"]).toBe("https://operations-api.access-ci.org");
});
it("should provide correct tools", () => {
const tools = server["getTools"]();
expect(tools).toHaveLength(5);
expect(tools.map(t => t.name)).toEqual([
"get_current_outages",
"get_scheduled_maintenance",
"get_past_outages",
"get_system_announcements",
"check_resource_status"
]);
});
it("should provide correct resources", () => {
const resources = server["getResources"]();
expect(resources).toHaveLength(4);
expect(resources.map(r => r.uri)).toEqual([
"accessci://system-status",
"accessci://outages/current",
"accessci://outages/scheduled",
"accessci://outages/past"
]);
});
});
describe("getCurrentOutages", () => {
it("should fetch and enhance current outages", async () => {
mockHttpClient.get.mockResolvedValue({
status: 200,
data: { results: mockCurrentOutagesData }
});
const result = await server["handleToolCall"]({
params: { name: "get_current_outages", arguments: {} }
});
expect(mockHttpClient.get).toHaveBeenCalledWith("/wh2/news/v1/affiliation/access-ci.org/current_outages/");
const response = JSON.parse(result.content[0].text);
expect(response.total_outages).toBe(2);
expect(response.affected_resources).toEqual(["Anvil", "Bridges-2"]);
expect(response.severity_counts).toHaveProperty("high", 1); // Emergency
expect(response.severity_counts).toHaveProperty("low", 1); // Scheduled maintenance
expect(response.outages[0]).toHaveProperty("severity");
expect(response.outages[0]).toHaveProperty("posted_time");
});
it("should filter outages by resource", async () => {
mockHttpClient.get.mockResolvedValue({
status: 200,
data: { results: mockCurrentOutagesData }
});
const result = await server["handleToolCall"]({
params: {
name: "get_current_outages",
arguments: { resource_filter: "Anvil" }
}
});
const response = JSON.parse(result.content[0].text);
expect(response.total_outages).toBe(1);
expect(response.outages[0].Subject).toContain("Anvil");
});
it("should categorize severity correctly", async () => {
mockHttpClient.get.mockResolvedValue({
status: 200,
data: { results: mockCurrentOutagesData }
});
const result = await server["handleToolCall"]({
params: { name: "get_current_outages", arguments: {} }
});
const response = JSON.parse(result.content[0].text);
const emergencyOutage = response.outages.find((o) => o.Subject.includes("Emergency"));
const maintenanceOutage = response.outages.find((o) => o.Subject.includes("Scheduled"));
expect(emergencyOutage.severity).toBe("high");
expect(maintenanceOutage.severity).toBe("low");
});
});
describe("getScheduledMaintenance", () => {
it("should fetch and enhance scheduled maintenance", async () => {
mockHttpClient.get.mockResolvedValue({
status: 200,
data: { results: mockFutureOutagesData }
});
const result = await server["handleToolCall"]({
params: { name: "get_scheduled_maintenance", arguments: {} }
});
const response = JSON.parse(result.content[0].text);
expect(response.total_scheduled).toBe(1);
expect(response.affected_resources).toEqual(["Jetstream"]);
expect(response.maintenance[0]).toHaveProperty("hours_until_start");
expect(response.maintenance[0]).toHaveProperty("duration_hours", 4); // 10am to 2pm = 4 hours
expect(response.maintenance[0]).toHaveProperty("has_scheduled_time", true);
});
it("should handle missing scheduled times", async () => {
const dataWithoutSchedule = [{
...mockFutureOutagesData[0],
OutageStartDateTime: null,
OutageEndDateTime: null
}];
mockHttpClient.get.mockResolvedValue({
status: 200,
data: { results: dataWithoutSchedule }
});
const result = await server["handleToolCall"]({
params: { name: "get_scheduled_maintenance", arguments: {} }
});
const response = JSON.parse(result.content[0].text);
expect(response.maintenance[0].has_scheduled_time).toBe(false);
expect(response.maintenance[0].duration_hours).toBe(null);
});
it("should sort by scheduled start time", async () => {
const multipleMaintenanceData = [
{
...mockFutureOutagesData[0],
OutageStartDateTime: "2024-08-31T10:00:00Z", // Later
Subject: "Later maintenance"
},
{
...mockFutureOutagesData[0],
OutageStartDateTime: "2024-08-30T10:00:00Z", // Earlier
Subject: "Earlier maintenance"
}
];
mockHttpClient.get.mockResolvedValue({
status: 200,
data: { results: multipleMaintenanceData }
});
const result = await server["handleToolCall"]({
params: { name: "get_scheduled_maintenance", arguments: {} }
});
const response = JSON.parse(result.content[0].text);
expect(response.maintenance[0].Subject).toBe("Earlier maintenance");
expect(response.maintenance[1].Subject).toBe("Later maintenance");
});
});
describe("getPastOutages", () => {
it("should fetch and enhance past outages", async () => {
mockHttpClient.get.mockResolvedValue({
status: 200,
data: { results: mockPastOutagesData }
});
const result = await server["handleToolCall"]({
params: { name: "get_past_outages", arguments: {} }
});
const response = JSON.parse(result.content[0].text);
expect(response.total_past_outages).toBe(1);
expect(response.outage_types).toEqual(["Full"]);
expect(response.average_duration_hours).toBe(6); // 6 hour duration
expect(response.outages[0]).toHaveProperty("duration_hours", 6);
expect(response.outages[0]).toHaveProperty("days_ago");
});
it("should apply limit correctly", async () => {
const manyOutages = Array(50).fill(0).map((_, i) => ({
...mockPastOutagesData[0],
id: `past-${i}`,
Subject: `Past outage ${i}`
}));
mockHttpClient.get.mockResolvedValue({
status: 200,
data: { results: manyOutages }
});
const result = await server["handleToolCall"]({
params: {
name: "get_past_outages",
arguments: { limit: 10 }
}
});
const response = JSON.parse(result.content[0].text);
expect(response.outages).toHaveLength(10);
});
});
describe("getSystemAnnouncements", () => {
it("should combine current, future, and recent past outages", async () => {
mockHttpClient.get
.mockResolvedValueOnce({ status: 200, data: { results: mockCurrentOutagesData } })
.mockResolvedValueOnce({ status: 200, data: { results: mockFutureOutagesData } })
.mockResolvedValueOnce({ status: 200, data: { results: mockPastOutagesData } });
const result = await server["handleToolCall"]({
params: { name: "get_system_announcements", arguments: {} }
});
expect(mockHttpClient.get).toHaveBeenCalledTimes(3);
const response = JSON.parse(result.content[0].text);
expect(response.current_outages).toBe(2);
expect(response.scheduled_maintenance).toBe(1);
expect(response.recent_past_outages).toBe(1); // Within 30 days
expect(response.categories).toHaveProperty("current");
expect(response.categories).toHaveProperty("scheduled");
expect(response.categories).toHaveProperty("recent_past");
});
it("should prioritize current outages in sorting", async () => {
mockHttpClient.get
.mockResolvedValueOnce({ status: 200, data: { results: mockCurrentOutagesData } })
.mockResolvedValueOnce({ status: 200, data: { results: mockFutureOutagesData } })
.mockResolvedValueOnce({ status: 200, data: { results: mockPastOutagesData } });
const result = await server["handleToolCall"]({
params: { name: "get_system_announcements", arguments: {} }
});
const response = JSON.parse(result.content[0].text);
const firstAnnouncement = response.announcements[0];
expect(firstAnnouncement.category).toBe("current");
});
});
describe("checkResourceStatus", () => {
it("should check resource status efficiently (direct method)", async () => {
mockHttpClient.get.mockResolvedValue({
status: 200,
data: { results: mockCurrentOutagesData }
});
const result = await server["handleToolCall"]({
params: {
name: "check_resource_status",
arguments: { resource_ids: ["anvil-1", "unknown-resource"] }
}
});
const response = JSON.parse(result.content[0].text);
expect(response.api_method).toBe("direct_outages_check");
expect(response.resources_checked).toBe(2);
expect(response.operational).toBe(1); // unknown-resource
expect(response.affected).toBe(1); // anvil-1
const anvilStatus = response.resource_status.find((r) => r.resource_id === "anvil-1");
expect(anvilStatus.status).toBe("affected");
expect(anvilStatus.severity).toBe("high"); // Emergency maintenance
});
it("should use group API when requested", async () => {
mockHttpClient.get.mockResolvedValue({
status: 200,
data: { results: [] } // No outages for this group
});
const result = await server["handleToolCall"]({
params: {
name: "check_resource_status",
arguments: {
resource_ids: ["anvil"],
use_group_api: true
}
}
});
expect(mockHttpClient.get).toHaveBeenCalledWith("/wh2/news/v1/info_groupid/anvil/");
const response = JSON.parse(result.content[0].text);
expect(response.api_method).toBe("resource_group_api");
expect(response.resource_status[0].status).toBe("operational");
expect(response.resource_status[0].api_method).toBe("group_specific");
});
it("should handle group API failures gracefully", async () => {
mockHttpClient.get.mockRejectedValue(new Error("API Error"));
const result = await server["handleToolCall"]({
params: {
name: "check_resource_status",
arguments: {
resource_ids: ["invalid-resource"],
use_group_api: true
}
}
});
const response = JSON.parse(result.content[0].text);
expect(response.unknown).toBe(1);
expect(response.resource_status[0].status).toBe("unknown");
expect(response.resource_status[0].api_method).toBe("group_specific_failed");
expect(response.resource_status[0]).toHaveProperty("error");
});
});
describe("Error Handling", () => {
it("should handle API errors gracefully", async () => {
mockHttpClient.get.mockResolvedValue({
status: 500,
statusText: "Internal Server Error"
});
const result = await server["handleToolCall"]({
params: { name: "get_current_outages", arguments: {} }
});
expect(result.content[0].text).toContain("Error");
});
it("should handle network errors", async () => {
mockHttpClient.get.mockRejectedValue(new Error("Network error"));
const result = await server["handleToolCall"]({
params: { name: "get_current_outages", arguments: {} }
});
expect(result.content[0].text).toContain("Error");
});
it("should handle unknown tools", async () => {
const result = await server["handleToolCall"]({
params: { name: "unknown_tool", arguments: {} }
});
expect(result.content[0].text).toContain("Unknown tool");
});
});
describe("Resource Handling", () => {
it("should handle resource reads correctly", async () => {
mockHttpClient.get.mockResolvedValue({
status: 200,
data: { results: mockCurrentOutagesData }
});
const result = await server["handleResourceRead"]({
params: { uri: "accessci://outages/current" }
});
expect(result.contents[0].mimeType).toBe("application/json");
expect(result.contents[0].text).toBeDefined();
});
it("should handle unknown resources", async () => {
await expect(async () => {
await server["handleResourceRead"]({
params: { uri: "accessci://unknown" }
});
}).rejects.toThrow("Unknown resource");
});
});
});