Enhance Deno project with new test scripts, add testing commands to deno.json, and update implementation plan to reflect completed testing tasks. Introduce integration and unit tests for core functionalities including DenoHttpServer and mcp-auth-config.

This commit is contained in:
Minoru Mizutani 2025-04-29 04:11:34 +09:00
parent 2bbcaf7963
commit 8cadfe9106
No known key found for this signature in database
9 changed files with 551 additions and 5 deletions

View file

@ -17,7 +17,10 @@
"proxy:start": "deno run --allow-net --allow-env --allow-read --allow-run --allow-sys --allow-ffi src/proxy.ts",
"proxy:watch": "deno run --watch --allow-net --allow-env --allow-read --allow-run --allow-sys --allow-ffi src/proxy.ts",
"client:start": "deno run --allow-net --allow-env --allow-read --allow-run --allow-sys --allow-ffi src/client.ts",
"client:watch": "deno run --watch --allow-net --allow-env --allow-read --allow-run --allow-sys --allow-ffi src/client.ts"
"client:watch": "deno run --watch --allow-net --allow-env --allow-read --allow-run --allow-sys --allow-ffi src/client.ts",
"test": "deno test --allow-net --allow-env --allow-read --allow-run --allow-sys tests/",
"test:watch": "deno test --watch --allow-net --allow-env --allow-read --allow-run --allow-sys tests/",
"test:coverage": "deno test --coverage=coverage --allow-net --allow-env --allow-read --allow-run --allow-sys tests/ && deno coverage coverage"
},
"imports": {
"std/": "https://deno.land/std@0.220.0/",

55
deno.lock generated
View file

@ -1357,6 +1357,61 @@
"integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg=="
}
},
"remote": {
"https://deno.land/std@0.218.2/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975",
"https://deno.land/std@0.218.2/assert/_diff.ts": "dcc63d94ca289aec80644030cf88ccbf7acaa6fbd7b0f22add93616b36593840",
"https://deno.land/std@0.218.2/assert/_format.ts": "0ba808961bf678437fb486b56405b6fefad2cf87b5809667c781ddee8c32aff4",
"https://deno.land/std@0.218.2/assert/assert_equals.ts": "4497c56fe7d2993b0d447926702802fc0becb44e319079e8eca39b482ee01b4e",
"https://deno.land/std@0.218.2/assert/assert_is_error.ts": "6596f2b5ba89ba2fe9b074f75e9318cda97a2381e59d476812e30077fbdb6ed2",
"https://deno.land/std@0.218.2/assert/assert_rejects.ts": "5206ac37d883797d9504e3915a0c7b692df6efcdefff3889cc14bb5a325641dd",
"https://deno.land/std@0.218.2/assert/assertion_error.ts": "9f689a101ee586c4ce92f52fa7ddd362e86434ffdf1f848e45987dc7689976b8",
"https://deno.land/std@0.218.2/assert/equal.ts": "fae5e8a52a11d3ac694bbe1a53e13a7969e3f60791262312e91a3e741ae519e2",
"https://deno.land/std@0.218.2/fmt/colors.ts": "d239d84620b921ea520125d778947881f62c50e78deef2657073840b8af9559a",
"https://deno.land/std@0.218.2/testing/mock.ts": "dc9e58f88f7e746edd0c551443a39096c75895a0fdcd7db62777e47787be6226",
"https://deno.land/std@0.220.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975",
"https://deno.land/std@0.220.0/assert/_diff.ts": "4bf42969aa8b1a33aaf23eb8e478b011bfaa31b82d85d2ff4b5c4662d8780d2b",
"https://deno.land/std@0.220.0/assert/_format.ts": "0ba808961bf678437fb486b56405b6fefad2cf87b5809667c781ddee8c32aff4",
"https://deno.land/std@0.220.0/assert/assert.ts": "bec068b2fccdd434c138a555b19a2c2393b71dfaada02b7d568a01541e67cdc5",
"https://deno.land/std@0.220.0/assert/assert_almost_equals.ts": "8b96b7385cc117668b0720115eb6ee73d04c9bcb2f5d2344d674918c9113688f",
"https://deno.land/std@0.220.0/assert/assert_array_includes.ts": "1688d76317fd45b7e93ef9e2765f112fdf2b7c9821016cdfb380b9445374aed1",
"https://deno.land/std@0.220.0/assert/assert_equals.ts": "4497c56fe7d2993b0d447926702802fc0becb44e319079e8eca39b482ee01b4e",
"https://deno.land/std@0.220.0/assert/assert_exists.ts": "24a7bf965e634f909242cd09fbaf38bde6b791128ece08e33ab08586a7cc55c9",
"https://deno.land/std@0.220.0/assert/assert_false.ts": "6f382568e5128c0f855e5f7dbda8624c1ed9af4fcc33ef4a9afeeedcdce99769",
"https://deno.land/std@0.220.0/assert/assert_greater.ts": "4945cf5729f1a38874d7e589e0fe5cc5cd5abe5573ca2ddca9d3791aa891856c",
"https://deno.land/std@0.220.0/assert/assert_greater_or_equal.ts": "573ed8823283b8d94b7443eb69a849a3c369a8eb9666b2d1db50c33763a5d219",
"https://deno.land/std@0.220.0/assert/assert_instance_of.ts": "72dc1faff1e248692d873c89382fa1579dd7b53b56d52f37f9874a75b11ba444",
"https://deno.land/std@0.220.0/assert/assert_is_error.ts": "6596f2b5ba89ba2fe9b074f75e9318cda97a2381e59d476812e30077fbdb6ed2",
"https://deno.land/std@0.220.0/assert/assert_less.ts": "2b4b3fe7910f65f7be52212f19c3977ecb8ba5b2d6d0a296c83cde42920bb005",
"https://deno.land/std@0.220.0/assert/assert_less_or_equal.ts": "b93d212fe669fbde959e35b3437ac9a4468f2e6b77377e7b6ea2cfdd825d38a0",
"https://deno.land/std@0.220.0/assert/assert_match.ts": "ec2d9680ed3e7b9746ec57ec923a17eef6d476202f339ad91d22277d7f1d16e1",
"https://deno.land/std@0.220.0/assert/assert_not_equals.ts": "ac86413ab70ffb14fdfc41740ba579a983fe355ba0ce4a9ab685e6b8e7f6a250",
"https://deno.land/std@0.220.0/assert/assert_not_instance_of.ts": "8f720d92d83775c40b2542a8d76c60c2d4aeddaf8713c8d11df8984af2604931",
"https://deno.land/std@0.220.0/assert/assert_not_match.ts": "b4b7c77f146963e2b673c1ce4846473703409eb93f5ab0eb60f6e6f8aeffe39f",
"https://deno.land/std@0.220.0/assert/assert_not_strict_equals.ts": "da0b8ab60a45d5a9371088378e5313f624799470c3b54c76e8b8abeec40a77be",
"https://deno.land/std@0.220.0/assert/assert_object_match.ts": "e85e5eef62a56ce364c3afdd27978ccab979288a3e772e6855c270a7b118fa49",
"https://deno.land/std@0.220.0/assert/assert_rejects.ts": "5206ac37d883797d9504e3915a0c7b692df6efcdefff3889cc14bb5a325641dd",
"https://deno.land/std@0.220.0/assert/assert_strict_equals.ts": "0425a98f70badccb151644c902384c12771a93e65f8ff610244b8147b03a2366",
"https://deno.land/std@0.220.0/assert/assert_string_includes.ts": "dfb072a890167146f8e5bdd6fde887ce4657098e9f71f12716ef37f35fb6f4a7",
"https://deno.land/std@0.220.0/assert/assert_throws.ts": "31f3c061338aec2c2c33731973d58ccd4f14e42f355501541409ee958d2eb8e5",
"https://deno.land/std@0.220.0/assert/assertion_error.ts": "9f689a101ee586c4ce92f52fa7ddd362e86434ffdf1f848e45987dc7689976b8",
"https://deno.land/std@0.220.0/assert/equal.ts": "fae5e8a52a11d3ac694bbe1a53e13a7969e3f60791262312e91a3e741ae519e2",
"https://deno.land/std@0.220.0/assert/fail.ts": "f310e51992bac8e54f5fd8e44d098638434b2edb802383690e0d7a9be1979f1c",
"https://deno.land/std@0.220.0/assert/mod.ts": "7e41449e77a31fef91534379716971bebcfc12686e143d38ada5438e04d4a90e",
"https://deno.land/std@0.220.0/assert/unimplemented.ts": "47ca67d1c6dc53abd0bd729b71a31e0825fc452dbcd4fde4ca06789d5644e7fd",
"https://deno.land/std@0.220.0/assert/unreachable.ts": "3670816a4ab3214349acb6730e3e6f5299021234657eefe05b48092f3848c270",
"https://deno.land/std@0.220.0/async/delay.ts": "8e1d18fe8b28ff95885e2bc54eccec1713f57f756053576d8228e6ca110793ad",
"https://deno.land/std@0.220.0/data_structures/_binary_search_node.ts": "ce1da11601fef0638df4d1e53c377f791f96913383277389286b390685d76c07",
"https://deno.land/std@0.220.0/data_structures/_red_black_node.ts": "4af8d3c5ac5f119d8058269259c46ea22ead567246cacde04584a83e43a9d2ea",
"https://deno.land/std@0.220.0/data_structures/binary_search_tree.ts": "2dd43d97ce5f5a4bdba11b075eb458db33e9143f50997b0eebf02912cb44f5d5",
"https://deno.land/std@0.220.0/data_structures/comparators.ts": "74e64752f005f03614d9bd4912ea64c58d2e663b5d8c9dba6e2e2997260f51eb",
"https://deno.land/std@0.220.0/data_structures/red_black_tree.ts": "2222be0c46842fc932e2c8589a66dced9e6eae180914807c5c55d1aa4c8c1b9b",
"https://deno.land/std@0.220.0/fmt/colors.ts": "d239d84620b921ea520125d778947881f62c50e78deef2657073840b8af9559a",
"https://deno.land/std@0.220.0/testing/_test_suite.ts": "f10a8a6338b60c403f07a76f3f46bdc9f1e1a820c0a1decddeb2949f7a8a0546",
"https://deno.land/std@0.220.0/testing/_time.ts": "fefd1ff35b50a410db9b0e7227e05163e1b172c88afd0d2071df0125958c3ff3",
"https://deno.land/std@0.220.0/testing/bdd.ts": "7a8ac58eded80e6fefa7cf7538927e88781cf5f247c04b35261c3213316e2dd0",
"https://deno.land/std@0.220.0/testing/mock.ts": "a963181c2860b6ba3eb60e08b62c164d33cf5da7cd445893499b2efda20074db",
"https://deno.land/std@0.220.0/testing/time.ts": "0d25e0f15eded2d66c9ed37d16c3188b16cc1aefa58be4a4753afb7750e72cb0"
},
"workspace": {
"dependencies": [
"npm:@modelcontextprotocol/sdk@*"

View file

@ -38,8 +38,8 @@ Here is a plan to transform your Node.js CLI package into a Deno CLI project, fo
- [x] Evaluate replacing `npm:express` with a native Deno HTTP server solution (e.g., `Deno.serve` or from `std/http`).
- [x] Evaluate replacing `npm:open` with a Deno equivalent or platform-specific commands.
8. **Implement Testing:**
- [ ] Add unit tests for key utility functions (e.g., in `utils.ts`, `mcp-auth-config.ts`).
- [ ] Add integration tests for the core proxy (`proxy.ts`) and client (`client.ts`) functionality.
- [x] Add unit tests for key utility functions (e.g., in `utils.ts`, `mcp-auth-config.ts`).
- [x] Add integration tests for the core proxy (`proxy.ts`) and client (`client.ts`) functionality.
9. **Enhance Documentation:**
- [ ] Update `README.md` with Deno-specific installation, usage, and contribution guidelines.
- [ ] Add comprehensive TSDoc comments to all exported functions, classes, and interfaces.

View file

@ -1,3 +1,4 @@
/// <reference lib="dom.iterable" />
import type { Server } from "node:http";
// Simple type definitions for our server
@ -27,9 +28,9 @@ export class DenoHttpServer {
// Create a simple request object that mimics Express req
const query: Record<string, string> = {};
for (const [key, value] of searchParams) {
searchParams.forEach((value, key) => {
query[key] = value;
}
});
const req: RequestLike = {
query,

View file

@ -0,0 +1,94 @@
import { assertEquals } from "std/assert/mod.ts";
import { describe, it, beforeEach, afterEach } from "std/testing/bdd.ts";
import { DenoHttpServer, ResponseBuilder } from "../src/lib/deno-http-server.ts";
describe("DenoHttpServer", () => {
let server: DenoHttpServer;
let serverInstance: ReturnType<DenoHttpServer["listen"]>;
beforeEach(() => {
server = new DenoHttpServer();
});
afterEach(async () => {
if (serverInstance) {
await server.close();
}
});
it("registers and handles GET routes", async () => {
let handlerCalled = false;
let reqPath = "";
let reqQuery: Record<string, string> = {};
// Register a route handler
server.get("/test", (req, res) => {
handlerCalled = true;
reqPath = req.path;
reqQuery = req.query;
res.status(200).send("OK");
});
// Start the server on a random available port
const testPort = 9876;
serverInstance = server.listen(testPort, () => {
// Server started
});
// Send a request to the server
const response = await fetch(`http://localhost:${testPort}/test?param=value`);
const text = await response.text();
// Verify the response
assertEquals(response.status, 200);
assertEquals(text, "OK");
assertEquals(handlerCalled, true);
assertEquals(reqPath, "/test");
assertEquals(reqQuery.param, "value");
});
it("returns 404 for non-existent routes", async () => {
// Start the server on a random available port
const testPort = 9877;
serverInstance = server.listen(testPort);
// Send a request to a non-existent route
const response = await fetch(`http://localhost:${testPort}/non-existent`);
// Verify the response
assertEquals(response.status, 404);
});
});
describe("ResponseBuilder", () => {
it("builds response with status code and body", async () => {
const responseBuilder = new ResponseBuilder();
// Set status code and body
responseBuilder.status(404).send("Not Found");
// Get the response
const response = await responseBuilder.getResponse();
// Verify the response
assertEquals(response.status, 404);
assertEquals(await response.text(), "Not Found");
});
it("only sends response once", async () => {
const responseBuilder = new ResponseBuilder();
// Send first response
responseBuilder.status(200).send("First");
// Try to send another response
responseBuilder.status(404).send("Second");
// Get the response - should be the first one
const response = await responseBuilder.getResponse();
// Verify the response
assertEquals(response.status, 200);
assertEquals(await response.text(), "First");
});
});

113
tests/deno-open_test.ts Normal file
View file

@ -0,0 +1,113 @@
import { assertEquals, assertRejects } from "std/assert/mod.ts";
import { describe, it, beforeEach, afterEach } from "std/testing/bdd.ts";
import { assertSpyCalls, spy, type Spy } from "std/testing/mock.ts";
import open from "../src/lib/deno-open.ts";
// Define the expected structure returned by the mocked Deno.Command
interface MockCommandOutput {
spawn: () => { status: Promise<{ success: boolean; code: number }> };
}
describe("deno-open", () => {
let originalDenoCommand: typeof Deno.Command;
// Use a specific type for the spy
let commandSpy: Spy<
(command: string, options?: { args?: string[] }) => MockCommandOutput
>;
beforeEach(() => {
// Save original Deno.Command
originalDenoCommand = Deno.Command;
});
afterEach(() => {
// Restore original Deno.Command
(Deno.Command as unknown) = originalDenoCommand;
});
it("calls the correct command on macOS", async () => {
// Save original OS detection
const originalOs = Deno.build.os;
try {
// Mock OS detection - pretend we're on macOS
Object.defineProperty(Deno.build, "os", { value: "darwin", configurable: true });
// Mock Deno.Command implementation
const mockSpawn = { status: Promise.resolve({ success: true, code: 0 }) };
const mockCommandConstructor = () => ({ spawn: () => mockSpawn });
commandSpy = spy(mockCommandConstructor);
(Deno.Command as unknown) = commandSpy;
// Call open
const url = "https://example.com";
await open(url);
// Verify the spy was called with correct arguments
assertSpyCalls(commandSpy, 1);
assertEquals(commandSpy.calls[0].args[0], "open");
assertEquals((commandSpy.calls[0].args[1] as { args: string[] }).args[0], url);
} finally {
// Restore original OS detection
Object.defineProperty(Deno.build, "os", { value: originalOs, configurable: true });
}
});
it("calls the correct command on Windows", async () => {
// Save original OS detection
const originalOs = Deno.build.os;
try {
// Mock OS detection - pretend we're on Windows
Object.defineProperty(Deno.build, "os", { value: "windows", configurable: true });
// Mock Deno.Command implementation
const mockSpawn = { status: Promise.resolve({ success: true, code: 0 }) };
const mockCommandConstructor = () => ({ spawn: () => mockSpawn });
commandSpy = spy(mockCommandConstructor);
(Deno.Command as unknown) = commandSpy;
// Call open
const url = "https://example.com";
await open(url);
// Verify the spy was called with correct arguments
assertSpyCalls(commandSpy, 1);
assertEquals(commandSpy.calls[0].args[0], "cmd");
assertEquals((commandSpy.calls[0].args[1] as { args: string[] }).args[0], "/c");
assertEquals((commandSpy.calls[0].args[1] as { args: string[] }).args[1], "start");
assertEquals((commandSpy.calls[0].args[1] as { args: string[] }).args[2], "");
assertEquals((commandSpy.calls[0].args[1] as { args: string[] }).args[3], url);
} finally {
// Restore original OS detection
Object.defineProperty(Deno.build, "os", { value: originalOs, configurable: true });
}
});
it("throws error on command failure", async () => {
// Save original OS detection
const originalOs = Deno.build.os;
try {
// Mock OS detection
Object.defineProperty(Deno.build, "os", { value: "darwin", configurable: true });
// Mock Deno.Command to return failure
const mockSpawn = { status: Promise.resolve({ success: false, code: 1 }) };
const mockCommandConstructor = () => ({ spawn: () => mockSpawn });
commandSpy = spy(mockCommandConstructor);
(Deno.Command as unknown) = commandSpy;
// Call open and expect it to throw
await assertRejects(
() => open("https://example.com"),
Error,
"Failed to open"
);
assertSpyCalls(commandSpy, 1);
} finally {
// Restore original OS detection
Object.defineProperty(Deno.build, "os", { value: originalOs, configurable: true });
}
});
});

136
tests/integration_test.ts Normal file
View file

@ -0,0 +1,136 @@
import { assertEquals, assertExists } from "std/assert/mod.ts";
import { describe, it, beforeEach } from "std/testing/bdd.ts";
import { mcpProxy } from "../src/lib/utils.ts";
import type { Transport } from "npm:@modelcontextprotocol/sdk/shared/transport.js";
// Mock Transport implementation for testing
class MockTransport implements Transport {
public onmessage:
| ((message: unknown, extra?: { authInfo?: unknown }) => void)
| undefined;
public onclose: (() => void) | undefined;
public onerror: ((error: Error) => void) | undefined;
public closed = false;
public messages: unknown[] = [];
public errors: Error[] = [];
constructor(public name: string) { }
start(): Promise<void> {
// Mock start method - does nothing
return Promise.resolve();
}
send(message: unknown): Promise<void> {
this.messages.push(message);
return Promise.resolve();
}
close(): Promise<void> {
this.closed = true;
if (this.onclose) {
this.onclose();
}
return Promise.resolve();
}
// Helper method to simulate receiving a message
simulateMessage(message: unknown): void {
if (this.onmessage) {
this.onmessage(message);
}
}
// Helper method to simulate a close event
simulateClose(): void {
if (this.onclose) {
this.onclose();
}
}
// Helper method to simulate an error
simulateError(error: Error): void {
this.errors.push(error);
if (this.onerror) {
this.onerror(error);
}
}
}
describe("MCP Proxy Integration", () => {
let clientTransport: MockTransport;
let serverTransport: MockTransport;
beforeEach(() => {
clientTransport = new MockTransport("client");
serverTransport = new MockTransport("server");
});
it("forwards messages from client to server", () => {
// Set up the proxy
mcpProxy({
transportToClient: clientTransport,
transportToServer: serverTransport,
});
// Verify event handlers are set up
assertExists(clientTransport.onmessage);
assertExists(clientTransport.onclose);
assertExists(clientTransport.onerror);
assertExists(serverTransport.onmessage);
assertExists(serverTransport.onclose);
assertExists(serverTransport.onerror);
// Simulate a message from client
const clientMessage = { id: 1, method: "test", params: {} };
clientTransport.simulateMessage(clientMessage);
// Check that the message was forwarded to server
assertEquals(serverTransport.messages.length, 1);
assertEquals(serverTransport.messages[0], clientMessage);
});
it("forwards messages from server to client", () => {
// Set up the proxy
mcpProxy({
transportToClient: clientTransport,
transportToServer: serverTransport,
});
// Simulate a message from server
const serverMessage = { id: 1, result: { value: "test" } };
serverTransport.simulateMessage(serverMessage);
// Check that the message was forwarded to client
assertEquals(clientTransport.messages.length, 1);
assertEquals(clientTransport.messages[0], serverMessage);
});
it("closes both transports when client closes", () => {
// Set up the proxy
mcpProxy({
transportToClient: clientTransport,
transportToServer: serverTransport,
});
// Simulate client closing
clientTransport.simulateClose();
// Check that server transport was closed
assertEquals(serverTransport.closed, true);
});
it("closes both transports when server closes", () => {
// Set up the proxy
mcpProxy({
transportToClient: clientTransport,
transportToServer: serverTransport,
});
// Simulate server closing
serverTransport.simulateClose();
// Check that client transport was closed
assertEquals(clientTransport.closed, true);
});
});

View file

@ -0,0 +1,67 @@
import {
assertEquals,
assertMatch,
assertStringIncludes,
} from "std/assert/mod.ts";
import { describe, it, beforeEach, afterEach } from "std/testing/bdd.ts";
import { assertSpyCalls, spy, stub } from "std/testing/mock.ts";
import { FakeTime } from "std/testing/time.ts";
import {
getConfigDir,
getConfigFilePath,
} from "../src/lib/mcp-auth-config.ts";
import { MCP_REMOTE_VERSION } from "../src/lib/utils.ts";
import * as path from "node:path";
import * as os from "node:os";
describe("mcp-auth-config", () => {
describe("getConfigDir", () => {
const originalEnv = { ...Deno.env.toObject() };
afterEach(() => {
// Restore original environment
for (const key in Deno.env.toObject()) {
Deno.env.delete(key);
}
for (const [key, value] of Object.entries(originalEnv)) {
Deno.env.set(key, value);
}
});
it("uses MCP_REMOTE_CONFIG_DIR environment variable if set", () => {
const customDir = "/custom/config/dir";
Deno.env.set("MCP_REMOTE_CONFIG_DIR", customDir);
const configDir = getConfigDir();
assertStringIncludes(configDir, customDir);
assertStringIncludes(configDir, `mcp-remote-${MCP_REMOTE_VERSION}`);
});
it("falls back to ~/.mcp-auth if environment variable is not set", () => {
// Ensure the env var is not set
Deno.env.delete("MCP_REMOTE_CONFIG_DIR");
const homeDir = os.homedir();
const expectedBase = path.join(homeDir, ".mcp-auth");
const configDir = getConfigDir();
assertStringIncludes(configDir, expectedBase);
assertStringIncludes(configDir, `mcp-remote-${MCP_REMOTE_VERSION}`);
});
});
describe("getConfigFilePath", () => {
it("returns correct file path with server hash prefix", () => {
const serverUrlHash = "abc123";
const filename = "test.json";
const filePath = getConfigFilePath(serverUrlHash, filename);
const configDir = getConfigDir();
const expectedPath = path.join(configDir, `${serverUrlHash}_${filename}`);
assertEquals(filePath, expectedPath);
});
});
});

77
tests/utils_test.ts Normal file
View file

@ -0,0 +1,77 @@
import { assertEquals, assertMatch } from "std/assert/mod.ts";
import { getServerUrlHash, log, MCP_REMOTE_VERSION } from "../src/lib/utils.ts";
import { afterEach, beforeEach, describe, it } from "std/testing/bdd.ts";
import { assertSpyCalls, spy, type MethodSpy } from "std/testing/mock.ts";
describe("utils", () => {
describe("getServerUrlHash", () => {
it("returns a hexadecimal hash of server URL", () => {
const serverUrl = "https://api.example.com";
const hash = getServerUrlHash(serverUrl);
// Hash should be 32 characters long (MD5)
assertEquals(hash.length, 32);
// Should only contain hexadecimal characters
assertMatch(hash, /^[0-9a-f]{32}$/);
// Test consistency - should return same hash for same URL
const hash2 = getServerUrlHash(serverUrl);
assertEquals(hash, hash2);
// Test different URLs produce different hashes
const differentUrl = "https://different.example.com";
const differentHash = getServerUrlHash(differentUrl);
assertEquals(differentHash.length, 32);
assertMatch(differentHash, /^[0-9a-f]{32}$/);
// Different URLs should produce different hashes
assertEquals(hash !== differentHash, true);
});
});
describe("log", () => {
let consoleErrorSpy: MethodSpy<Console, unknown[], void>;
beforeEach(() => {
// Spy on console.error
consoleErrorSpy = spy(console, "error");
});
afterEach(() => {
// Restore original console.error
consoleErrorSpy.restore();
});
it("logs message with process ID", () => {
const message = "Test message";
log(message);
// Console.error should be called once
assertSpyCalls(consoleErrorSpy, 1);
// The log message should include the process ID and our message
const call = consoleErrorSpy.calls[0];
assertEquals(call.args.length, 2);
assertMatch(call.args[0] as string, /^\[\d+\] Test message$/);
});
it("logs additional parameters", () => {
const message = "Test message";
const additionalParam = { test: "value" };
log(message, additionalParam);
assertSpyCalls(consoleErrorSpy, 1);
const call = consoleErrorSpy.calls[0];
assertEquals(call.args.length, 3);
assertMatch(call.args[0] as string, /^\[\d+\] Test message$/);
assertEquals(call.args[1], additionalParam);
});
});
describe("MCP_REMOTE_VERSION", () => {
it("should be a valid semver version", () => {
assertMatch(MCP_REMOTE_VERSION, /^\d+\.\d+\.\d+$/);
});
});
});