mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-22 00:05:51 +00:00
test: 添加一大堆测试文件
This commit is contained in:
69
src/services/mcp/__tests__/channelNotification.test.ts
Normal file
69
src/services/mcp/__tests__/channelNotification.test.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
// findChannelEntry extracted from ../channelNotification.ts (line 161)
|
||||
// Copied to avoid heavy import chain
|
||||
|
||||
type ChannelEntry = {
|
||||
kind: "server" | "plugin"
|
||||
name: string
|
||||
}
|
||||
|
||||
function findChannelEntry(
|
||||
serverName: string,
|
||||
channels: readonly ChannelEntry[],
|
||||
): ChannelEntry | undefined {
|
||||
const parts = serverName.split(":")
|
||||
return channels.find(c =>
|
||||
c.kind === "server"
|
||||
? serverName === c.name
|
||||
: parts[0] === "plugin" && parts[1] === c.name,
|
||||
)
|
||||
}
|
||||
|
||||
describe("findChannelEntry", () => {
|
||||
test("finds server entry by exact name match", () => {
|
||||
const channels = [{ kind: "server" as const, name: "my-server" }]
|
||||
expect(findChannelEntry("my-server", channels)).toBeDefined()
|
||||
expect(findChannelEntry("my-server", channels)!.name).toBe("my-server")
|
||||
})
|
||||
|
||||
test("finds plugin entry by matching second segment", () => {
|
||||
const channels = [{ kind: "plugin" as const, name: "slack" }]
|
||||
expect(findChannelEntry("plugin:slack:tg", channels)).toBeDefined()
|
||||
})
|
||||
|
||||
test("returns undefined for no match", () => {
|
||||
const channels = [{ kind: "server" as const, name: "other" }]
|
||||
expect(findChannelEntry("my-server", channels)).toBeUndefined()
|
||||
})
|
||||
|
||||
test("handles empty channels array", () => {
|
||||
expect(findChannelEntry("my-server", [])).toBeUndefined()
|
||||
})
|
||||
|
||||
test("handles server name without colon", () => {
|
||||
const channels = [{ kind: "server" as const, name: "simple" }]
|
||||
expect(findChannelEntry("simple", channels)).toBeDefined()
|
||||
})
|
||||
|
||||
test("handles 'plugin:name' format correctly", () => {
|
||||
const channels = [{ kind: "plugin" as const, name: "slack" }]
|
||||
expect(findChannelEntry("plugin:slack:tg", channels)).toBeDefined()
|
||||
expect(findChannelEntry("plugin:discord:tg", channels)).toBeUndefined()
|
||||
})
|
||||
|
||||
test("prefers exact match (server kind) over partial match", () => {
|
||||
const channels = [
|
||||
{ kind: "server" as const, name: "plugin:slack" },
|
||||
{ kind: "plugin" as const, name: "slack" },
|
||||
]
|
||||
const result = findChannelEntry("plugin:slack", channels)
|
||||
expect(result).toBeDefined()
|
||||
expect(result!.kind).toBe("server")
|
||||
})
|
||||
|
||||
test("plugin kind does not match bare name", () => {
|
||||
const channels = [{ kind: "plugin" as const, name: "slack" }]
|
||||
expect(findChannelEntry("slack", channels)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
165
src/services/mcp/__tests__/channelPermissions.test.ts
Normal file
165
src/services/mcp/__tests__/channelPermissions.test.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { mock, describe, expect, test } from "bun:test";
|
||||
|
||||
mock.module("src/utils/slowOperations.js", () => ({
|
||||
jsonStringify: (v: unknown) => JSON.stringify(v),
|
||||
}));
|
||||
mock.module("src/services/analytics/growthbook.js", () => ({
|
||||
getFeatureValue_CACHED_MAY_BE_STALE: () => false,
|
||||
}));
|
||||
|
||||
const {
|
||||
shortRequestId,
|
||||
truncateForPreview,
|
||||
PERMISSION_REPLY_RE,
|
||||
createChannelPermissionCallbacks,
|
||||
} = await import("../channelPermissions");
|
||||
|
||||
describe("shortRequestId", () => {
|
||||
test("returns 5-char string from tool use ID", () => {
|
||||
const result = shortRequestId("toolu_abc123");
|
||||
expect(result).toHaveLength(5);
|
||||
});
|
||||
|
||||
test("is deterministic (same input = same output)", () => {
|
||||
const a = shortRequestId("toolu_abc123");
|
||||
const b = shortRequestId("toolu_abc123");
|
||||
expect(a).toBe(b);
|
||||
});
|
||||
|
||||
test("different inputs produce different outputs", () => {
|
||||
const a = shortRequestId("toolu_aaa");
|
||||
const b = shortRequestId("toolu_bbb");
|
||||
expect(a).not.toBe(b);
|
||||
});
|
||||
|
||||
test("result contains only valid letters (no 'l')", () => {
|
||||
const validChars = new Set("abcdefghijkmnopqrstuvwxyz");
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const result = shortRequestId(`toolu_${i}`);
|
||||
for (const ch of result) {
|
||||
expect(validChars.has(ch)).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("handles empty string", () => {
|
||||
const result = shortRequestId("");
|
||||
expect(result).toHaveLength(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe("truncateForPreview", () => {
|
||||
test("returns JSON string for object input", () => {
|
||||
const result = truncateForPreview({ key: "value" });
|
||||
expect(result).toBe('{"key":"value"}');
|
||||
});
|
||||
|
||||
test("truncates to <=200 chars with ellipsis when input is long", () => {
|
||||
const longObj = { data: "x".repeat(300) };
|
||||
const result = truncateForPreview(longObj);
|
||||
expect(result.length).toBeLessThanOrEqual(203); // 200 + '…'
|
||||
expect(result.endsWith("…")).toBe(true);
|
||||
});
|
||||
|
||||
test("returns short input unchanged", () => {
|
||||
const result = truncateForPreview({ a: 1 });
|
||||
expect(result).toBe('{"a":1}');
|
||||
expect(result.endsWith("…")).toBe(false);
|
||||
});
|
||||
|
||||
test("handles string input", () => {
|
||||
const result = truncateForPreview("hello");
|
||||
expect(result).toBe('"hello"');
|
||||
});
|
||||
|
||||
test("handles null input", () => {
|
||||
const result = truncateForPreview(null);
|
||||
expect(result).toBe("null");
|
||||
});
|
||||
|
||||
test("handles undefined input", () => {
|
||||
const result = truncateForPreview(undefined);
|
||||
// JSON.stringify(undefined) returns undefined, then .length throws → catch returns '(unserializable)'
|
||||
expect(result).toBe("(unserializable)");
|
||||
});
|
||||
});
|
||||
|
||||
describe("PERMISSION_REPLY_RE", () => {
|
||||
test("matches 'y abcde'", () => {
|
||||
expect(PERMISSION_REPLY_RE.test("y abcde")).toBe(true);
|
||||
});
|
||||
|
||||
test("matches 'yes abcde'", () => {
|
||||
expect(PERMISSION_REPLY_RE.test("yes abcde")).toBe(true);
|
||||
});
|
||||
|
||||
test("matches 'n abcde'", () => {
|
||||
expect(PERMISSION_REPLY_RE.test("n abcde")).toBe(true);
|
||||
});
|
||||
|
||||
test("matches 'no abcde'", () => {
|
||||
expect(PERMISSION_REPLY_RE.test("no abcde")).toBe(true);
|
||||
});
|
||||
|
||||
test("is case-insensitive", () => {
|
||||
expect(PERMISSION_REPLY_RE.test("Y abcde")).toBe(true);
|
||||
expect(PERMISSION_REPLY_RE.test("YES abcde")).toBe(true);
|
||||
});
|
||||
|
||||
test("does not match without ID", () => {
|
||||
expect(PERMISSION_REPLY_RE.test("yes")).toBe(false);
|
||||
});
|
||||
|
||||
test("captures the ID from reply", () => {
|
||||
const match = "y abcde".match(PERMISSION_REPLY_RE);
|
||||
expect(match?.[2]).toBe("abcde");
|
||||
});
|
||||
});
|
||||
|
||||
describe("createChannelPermissionCallbacks", () => {
|
||||
test("resolve returns false for unknown request ID", () => {
|
||||
const cb = createChannelPermissionCallbacks();
|
||||
expect(cb.resolve("unknown-id", "allow", "server")).toBe(false);
|
||||
});
|
||||
|
||||
test("onResponse + resolve triggers handler", () => {
|
||||
const cb = createChannelPermissionCallbacks();
|
||||
let received: any = null;
|
||||
cb.onResponse("test-id", (response) => {
|
||||
received = response;
|
||||
});
|
||||
expect(cb.resolve("test-id", "allow", "test-server")).toBe(true);
|
||||
expect(received).toEqual({
|
||||
behavior: "allow",
|
||||
fromServer: "test-server",
|
||||
});
|
||||
});
|
||||
|
||||
test("onResponse unsubscribe prevents resolve", () => {
|
||||
const cb = createChannelPermissionCallbacks();
|
||||
let called = false;
|
||||
const unsub = cb.onResponse("test-id", () => {
|
||||
called = true;
|
||||
});
|
||||
unsub();
|
||||
expect(cb.resolve("test-id", "allow", "server")).toBe(false);
|
||||
expect(called).toBe(false);
|
||||
});
|
||||
|
||||
test("duplicate resolve returns false (already consumed)", () => {
|
||||
const cb = createChannelPermissionCallbacks();
|
||||
cb.onResponse("test-id", () => {});
|
||||
expect(cb.resolve("test-id", "allow", "server")).toBe(true);
|
||||
expect(cb.resolve("test-id", "allow", "server")).toBe(false);
|
||||
});
|
||||
|
||||
test("is case-insensitive for request IDs", () => {
|
||||
const cb = createChannelPermissionCallbacks();
|
||||
let received: any = null;
|
||||
cb.onResponse("ABC", (response) => {
|
||||
received = response;
|
||||
});
|
||||
expect(cb.resolve("abc", "deny", "server")).toBe(true);
|
||||
expect(received?.behavior).toBe("deny");
|
||||
});
|
||||
});
|
||||
65
src/services/mcp/__tests__/filterUtils.test.ts
Normal file
65
src/services/mcp/__tests__/filterUtils.test.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
// parseHeaders is a pure function from ../utils.ts (line 325)
|
||||
// Copied here to avoid triggering the heavy import chain of utils.ts
|
||||
function parseHeaders(headerArray: string[]): Record<string, string> {
|
||||
const headers: Record<string, string> = {}
|
||||
for (const header of headerArray) {
|
||||
const colonIndex = header.indexOf(":")
|
||||
if (colonIndex === -1) {
|
||||
throw new Error(
|
||||
`Invalid header format: "${header}". Expected format: "Header-Name: value"`,
|
||||
)
|
||||
}
|
||||
const key = header.substring(0, colonIndex).trim()
|
||||
const value = header.substring(colonIndex + 1).trim()
|
||||
if (!key) {
|
||||
throw new Error(
|
||||
`Invalid header: "${header}". Header name cannot be empty.`,
|
||||
)
|
||||
}
|
||||
headers[key] = value
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
describe("parseHeaders", () => {
|
||||
test("parses 'Key: Value' format", () => {
|
||||
expect(parseHeaders(["Content-Type: application/json"])).toEqual({
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
});
|
||||
|
||||
test("parses multiple headers", () => {
|
||||
expect(parseHeaders(["Key1: val1", "Key2: val2"])).toEqual({
|
||||
Key1: "val1",
|
||||
Key2: "val2",
|
||||
});
|
||||
});
|
||||
|
||||
test("trims whitespace around key and value", () => {
|
||||
expect(parseHeaders([" Key : Value "])).toEqual({ Key: "Value" });
|
||||
});
|
||||
|
||||
test("throws on missing colon", () => {
|
||||
expect(() => parseHeaders(["no colon here"])).toThrow();
|
||||
});
|
||||
|
||||
test("throws on empty key", () => {
|
||||
expect(() => parseHeaders([": value"])).toThrow();
|
||||
});
|
||||
|
||||
test("handles value with colons (like URLs)", () => {
|
||||
expect(parseHeaders(["url: http://example.com:8080"])).toEqual({
|
||||
url: "http://example.com:8080",
|
||||
});
|
||||
});
|
||||
|
||||
test("returns empty object for empty array", () => {
|
||||
expect(parseHeaders([])).toEqual({});
|
||||
});
|
||||
|
||||
test("handles duplicate keys (last wins)", () => {
|
||||
expect(parseHeaders(["K: v1", "K: v2"])).toEqual({ K: "v2" });
|
||||
});
|
||||
});
|
||||
45
src/services/mcp/__tests__/officialRegistry.test.ts
Normal file
45
src/services/mcp/__tests__/officialRegistry.test.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { mock, describe, expect, test, afterEach } from "bun:test";
|
||||
|
||||
mock.module("axios", () => ({
|
||||
default: { get: async () => ({ data: { servers: [] } }) },
|
||||
}));
|
||||
mock.module("src/utils/debug.js", () => ({
|
||||
logForDebugging: () => {},
|
||||
}));
|
||||
mock.module("src/utils/errors.js", () => ({
|
||||
errorMessage: (e: any) => String(e),
|
||||
}));
|
||||
|
||||
const { isOfficialMcpUrl, resetOfficialMcpUrlsForTesting } = await import(
|
||||
"../officialRegistry"
|
||||
);
|
||||
|
||||
describe("isOfficialMcpUrl", () => {
|
||||
afterEach(() => {
|
||||
resetOfficialMcpUrlsForTesting();
|
||||
});
|
||||
|
||||
test("returns false when registry not loaded (initial state)", () => {
|
||||
resetOfficialMcpUrlsForTesting();
|
||||
expect(isOfficialMcpUrl("https://example.com")).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false for non-registered URL", () => {
|
||||
expect(isOfficialMcpUrl("https://random-server.com/mcp")).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false for empty string", () => {
|
||||
expect(isOfficialMcpUrl("")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resetOfficialMcpUrlsForTesting", () => {
|
||||
test("can be called without error", () => {
|
||||
expect(() => resetOfficialMcpUrlsForTesting()).not.toThrow();
|
||||
});
|
||||
|
||||
test("clears state so subsequent lookups return false", () => {
|
||||
resetOfficialMcpUrlsForTesting();
|
||||
expect(isOfficialMcpUrl("https://anything.com")).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user