test: 添加一大堆测试文件

This commit is contained in:
claude-code-best
2026-04-02 20:28:08 +08:00
parent 6f5623b26c
commit ce29527a67
33 changed files with 3502 additions and 329 deletions

View File

@@ -0,0 +1,136 @@
import { mock, describe, expect, test } from "bun:test";
// Mock heavy deps
mock.module("../../utils/model/agent.js", () => ({
getDefaultSubagentModel: () => undefined,
}));
mock.module("../../utils/settings/constants.js", () => ({
getSourceDisplayName: (source: string) => source,
}));
const {
resolveAgentOverrides,
compareAgentsByName,
AGENT_SOURCE_GROUPS,
} = await import("../agentDisplay");
function makeAgent(agentType: string, source: string): any {
return { agentType, source, name: agentType };
}
describe("resolveAgentOverrides", () => {
test("marks no overrides when all agents active", () => {
const agents = [makeAgent("builder", "userSettings")];
const result = resolveAgentOverrides(agents, agents);
expect(result).toHaveLength(1);
expect(result[0].overriddenBy).toBeUndefined();
});
test("marks inactive agent as overridden", () => {
const allAgents = [
makeAgent("builder", "projectSettings"),
makeAgent("builder", "userSettings"),
];
const activeAgents = [makeAgent("builder", "userSettings")];
const result = resolveAgentOverrides(allAgents, activeAgents);
const projectAgent = result.find(
(a: any) => a.source === "projectSettings",
);
expect(projectAgent?.overriddenBy).toBe("userSettings");
});
test("overriddenBy shows the overriding agent source", () => {
const allAgents = [makeAgent("tester", "localSettings")];
const activeAgents = [makeAgent("tester", "policySettings")];
const result = resolveAgentOverrides(allAgents, activeAgents);
expect(result[0].overriddenBy).toBe("policySettings");
});
test("deduplicates agents by (agentType, source)", () => {
const agents = [
makeAgent("builder", "userSettings"),
makeAgent("builder", "userSettings"), // duplicate
];
const result = resolveAgentOverrides(agents, agents.slice(0, 1));
expect(result).toHaveLength(1);
});
test("preserves agent definition properties", () => {
const agents = [{ agentType: "a", source: "userSettings", name: "Agent A" }];
const result = resolveAgentOverrides(agents, agents);
expect(result[0].name).toBe("Agent A");
expect(result[0].agentType).toBe("a");
});
test("handles empty arrays", () => {
expect(resolveAgentOverrides([], [])).toEqual([]);
});
test("handles agent from git worktree (duplicate detection)", () => {
const agents = [
makeAgent("builder", "projectSettings"),
makeAgent("builder", "projectSettings"),
makeAgent("builder", "localSettings"),
];
const result = resolveAgentOverrides(agents, agents.slice(0, 1));
// Deduped: projectSettings appears once, localSettings once
expect(result).toHaveLength(2);
});
});
describe("compareAgentsByName", () => {
test("sorts alphabetically ascending", () => {
const a = makeAgent("alpha", "userSettings");
const b = makeAgent("beta", "userSettings");
expect(compareAgentsByName(a, b)).toBeLessThan(0);
});
test("returns negative when a.name < b.name", () => {
const a = makeAgent("a", "s");
const b = makeAgent("b", "s");
expect(compareAgentsByName(a, b)).toBeLessThan(0);
});
test("returns positive when a.name > b.name", () => {
const a = makeAgent("z", "s");
const b = makeAgent("a", "s");
expect(compareAgentsByName(a, b)).toBeGreaterThan(0);
});
test("returns 0 for same name", () => {
const a = makeAgent("same", "s");
const b = makeAgent("same", "s");
expect(compareAgentsByName(a, b)).toBe(0);
});
test("is case-insensitive (sensitivity: base)", () => {
const a = makeAgent("Alpha", "s");
const b = makeAgent("alpha", "s");
expect(compareAgentsByName(a, b)).toBe(0);
});
});
describe("AGENT_SOURCE_GROUPS", () => {
test("contains expected source groups in order", () => {
expect(AGENT_SOURCE_GROUPS).toHaveLength(7);
expect(AGENT_SOURCE_GROUPS[0]).toEqual({
label: "User agents",
source: "userSettings",
});
expect(AGENT_SOURCE_GROUPS[6]).toEqual({
label: "Built-in agents",
source: "built-in",
});
});
test("has unique labels", () => {
const labels = AGENT_SOURCE_GROUPS.map((g) => g.label);
expect(new Set(labels).size).toBe(labels.length);
});
test("has unique sources", () => {
const sources = AGENT_SOURCE_GROUPS.map((g) => g.source);
expect(new Set(sources).size).toBe(sources.length);
});
});

View File

@@ -0,0 +1,314 @@
import { mock, describe, expect, test } from "bun:test";
// ─── Comprehensive mocks for agentToolUtils.ts dependencies ───
// These must cover ALL named exports used by the module's transitive imports.
const noop = () => {};
const emptySet = () => new Set<string>();
// Utility: create a mock module factory that returns an object with arbitrary named exports
function stubModule(exportNames: string[]) {
const obj: Record<string, any> = {};
for (const name of exportNames) {
obj[name] = noop;
}
return () => obj;
}
mock.module("bun:bundle", () => ({ feature: () => false }));
mock.module("zod/v4", () => ({
z: {
object: () => ({ extend: () => ({ parse: noop }) }),
strictObject: () => ({ extend: noop }),
string: () => ({ optional: () => ({ describe: noop }) }),
number: () => ({ optional: noop }),
boolean: () => ({ describe: noop }),
enum: () => ({ optional: noop }),
array: noop,
union: noop,
optional: noop,
preprocess: noop,
nullable: noop,
record: noop,
any: noop,
unknown: noop,
default: noop,
},
}));
mock.module("src/bootstrap/state.js", () => ({
clearInvokedSkillsForAgent: noop,
}));
mock.module("src/constants/tools.js", () => ({
ALL_AGENT_DISALLOWED_TOOLS: new Set(),
ASYNC_AGENT_ALLOWED_TOOLS: new Set(),
CUSTOM_AGENT_DISALLOWED_TOOLS: new Set(),
IN_PROCESS_TEAMMATE_ALLOWED_TOOLS: new Set(),
}));
mock.module("src/services/AgentSummary/agentSummary.js", () => ({
startAgentSummarization: noop,
}));
mock.module("src/services/analytics/index.js", () => ({
logEvent: noop,
AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS: undefined,
}));
mock.module("src/services/api/dumpPrompts.js", () => ({
clearDumpState: noop,
}));
mock.module("src/Tool.js", () => ({
toolMatchesName: () => false,
findToolByName: noop,
toolMatchesName: () => false,
}));
// messages.ts is complex - provide stubs for all named exports
mock.module("src/utils/messages.ts", () => ({
extractTextContent: (content: any[]) =>
content?.filter?.((b: any) => b.type === "text")?.map?.((b: any) => b.text)?.join("") ?? "",
getLastAssistantMessage: () => null,
SYNTHETIC_MESSAGES: new Set(),
INTERRUPT_MESSAGE: "",
INTERRUPT_MESSAGE_FOR_TOOL_USE: "",
CANCEL_MESSAGE: "",
REJECT_MESSAGE: "",
REJECT_MESSAGE_WITH_REASON_PREFIX: "",
SUBAGENT_REJECT_MESSAGE: "",
SUBAGENT_REJECT_MESSAGE_WITH_REASON_PREFIX: "",
PLAN_REJECTION_PREFIX: "",
DENIAL_WORKAROUND_GUIDANCE: "",
NO_RESPONSE_REQUESTED: "",
SYNTHETIC_TOOL_RESULT_PLACEHOLDER: "",
SYNTHETIC_MODEL: "",
AUTO_REJECT_MESSAGE: noop,
DONT_ASK_REJECT_MESSAGE: noop,
withMemoryCorrectionHint: (s: string) => s,
deriveShortMessageId: () => "",
isClassifierDenial: () => false,
buildYoloRejectionMessage: () => "",
buildClassifierUnavailableMessage: () => "",
isEmptyMessageText: () => true,
createAssistantMessage: noop,
createAssistantAPIErrorMessage: noop,
createUserMessage: noop,
prepareUserContent: noop,
createUserInterruptionMessage: noop,
createSyntheticUserCaveatMessage: noop,
formatCommandInputTags: noop,
}));
mock.module("src/tasks/LocalAgentTask/LocalAgentTask.js", () => ({
completeAgentTask: noop,
createActivityDescriptionResolver: () => ({}),
createProgressTracker: () => ({}),
enqueueAgentNotification: noop,
failAgentTask: noop,
getProgressUpdate: () => ({ tokenCount: 0, toolUseCount: 0 }),
getTokenCountFromTracker: () => 0,
isLocalAgentTask: () => false,
killAsyncAgent: noop,
updateAgentProgress: noop,
updateProgressFromMessage: noop,
}));
mock.module("src/utils/agentSwarmsEnabled.js", () => ({
isAgentSwarmsEnabled: () => false,
}));
mock.module("src/utils/debug.js", () => ({
logForDebugging: noop,
}));
mock.module("src/utils/envUtils.js", () => ({
isInProtectedNamespace: () => false,
}));
mock.module("src/utils/errors.js", () => ({
AbortError: class extends Error {},
errorMessage: (e: any) => String(e),
}));
mock.module("src/utils/forkedAgent.js", () => ({}));
mock.module("src/utils/lazySchema.js", () => ({
lazySchema: (fn: () => any) => fn,
}));
mock.module("src/utils/permissions/PermissionMode.js", () => ({}));
// Provide working permissionRuleValueFromString to avoid polluting other test files
const LEGACY_ALIASES: Record<string, string> = {
Task: "Agent",
KillShell: "TaskStop",
AgentOutputTool: "TaskOutput",
BashOutputTool: "TaskOutput",
};
function normalizeLegacyToolName(name: string): string {
return LEGACY_ALIASES[name] ?? name;
}
function escapeRuleContent(content: string): string {
return content.replace(/\\/g, "\\\\").replace(/\(/g, "\\(").replace(/\)/g, "\\)");
}
function unescapeRuleContent(content: string): string {
return content.replace(/\\\(/g, "(").replace(/\\\)/g, ")").replace(/\\\\/g, "\\");
}
mock.module("src/utils/permissions/permissionRuleParser.js", () => ({
permissionRuleValueFromString: (ruleString: string) => {
const openIdx = ruleString.indexOf("(");
if (openIdx === -1) return { toolName: normalizeLegacyToolName(ruleString) };
const closeIdx = ruleString.lastIndexOf(")");
if (closeIdx === -1 || closeIdx <= openIdx) return { toolName: normalizeLegacyToolName(ruleString) };
if (closeIdx !== ruleString.length - 1) return { toolName: normalizeLegacyToolName(ruleString) };
const toolName = ruleString.substring(0, openIdx);
const rawContent = ruleString.substring(openIdx + 1, closeIdx);
if (!toolName) return { toolName: normalizeLegacyToolName(ruleString) };
if (rawContent === "" || rawContent === "*") return { toolName: normalizeLegacyToolName(toolName) };
return { toolName: normalizeLegacyToolName(toolName), ruleContent: unescapeRuleContent(rawContent) };
},
permissionRuleValueToString: (v: any) => {
if (!v.ruleContent) return v.toolName;
return `${v.toolName}(${escapeRuleContent(v.ruleContent)})`;
},
normalizeLegacyToolName,
}));
mock.module("src/utils/permissions/yoloClassifier.js", () => ({
buildTranscriptForClassifier: () => "",
classifyYoloAction: () => null,
}));
mock.module("src/utils/task/sdkProgress.js", () => ({
emitTaskProgress: noop,
}));
mock.module("src/utils/teammateContext.js", () => ({
isInProcessTeammate: () => false,
}));
mock.module("src/utils/tokens.js", () => ({
getTokenCountFromUsage: () => 0,
}));
mock.module("src/tools/ExitPlanModeTool/constants.js", () => ({
EXIT_PLAN_MODE_V2_TOOL_NAME: "exit_plan_mode",
}));
mock.module("src/tools/AgentTool/constants.js", () => ({
AGENT_TOOL_NAME: "agent",
LEGACY_AGENT_TOOL_NAME: "task",
}));
mock.module("src/tools/AgentTool/loadAgentsDir.js", () => ({}));
mock.module("src/state/AppState.js", () => ({}));
mock.module("src/types/ids.js", () => ({
asAgentId: (id: string) => id,
}));
// Break circular dep
mock.module("src/tools/AgentTool/AgentTool.tsx", () => ({
AgentTool: {},
inputSchema: {},
outputSchema: {},
default: {},
}));
const {
countToolUses,
getLastToolUseName,
} = await import("../agentToolUtils");
function makeAssistantMessage(content: any[]): any {
return { type: "assistant", message: { content } };
}
function makeUserMessage(text: string): any {
return { type: "user", message: { content: text } };
}
describe("countToolUses", () => {
test("counts tool_use blocks in messages", () => {
const messages = [
makeAssistantMessage([
{ type: "tool_use", name: "Read" },
{ type: "text", text: "hello" },
]),
];
expect(countToolUses(messages)).toBe(1);
});
test("returns 0 for messages without tool_use", () => {
const messages = [
makeAssistantMessage([{ type: "text", text: "hello" }]),
];
expect(countToolUses(messages)).toBe(0);
});
test("returns 0 for empty array", () => {
expect(countToolUses([])).toBe(0);
});
test("counts multiple tool_use blocks across messages", () => {
const messages = [
makeAssistantMessage([{ type: "tool_use", name: "Read" }]),
makeUserMessage("ok"),
makeAssistantMessage([{ type: "tool_use", name: "Write" }]),
];
expect(countToolUses(messages)).toBe(2);
});
test("counts tool_use in single message with multiple blocks", () => {
const messages = [
makeAssistantMessage([
{ type: "tool_use", name: "Read" },
{ type: "tool_use", name: "Grep" },
{ type: "tool_use", name: "Write" },
]),
];
expect(countToolUses(messages)).toBe(3);
});
});
describe("getLastToolUseName", () => {
test("returns last tool name from assistant message", () => {
const msg = makeAssistantMessage([
{ type: "tool_use", name: "Read" },
{ type: "tool_use", name: "Write" },
]);
expect(getLastToolUseName(msg)).toBe("Write");
});
test("returns undefined for message without tool_use", () => {
const msg = makeAssistantMessage([{ type: "text", text: "hello" }]);
expect(getLastToolUseName(msg)).toBeUndefined();
});
test("returns the last tool when multiple tool_uses present", () => {
const msg = makeAssistantMessage([
{ type: "tool_use", name: "Read" },
{ type: "tool_use", name: "Grep" },
{ type: "tool_use", name: "Edit" },
]);
expect(getLastToolUseName(msg)).toBe("Edit");
});
test("returns undefined for non-assistant message", () => {
const msg = makeUserMessage("hello");
expect(getLastToolUseName(msg)).toBeUndefined();
});
test("handles message with null content", () => {
const msg = { type: "assistant", message: { content: null } };
expect(getLastToolUseName(msg)).toBeUndefined();
});
});

View File

@@ -0,0 +1,146 @@
import { describe, expect, test } from "bun:test";
import { classifyMcpToolForCollapse } from "../classifyForCollapse";
describe("classifyMcpToolForCollapse", () => {
// Search tools
test("classifies Slack slack_search_public as search", () => {
expect(classifyMcpToolForCollapse("slack", "slack_search_public")).toEqual({
isSearch: true,
isRead: false,
});
});
test("classifies GitHub search_code as search", () => {
expect(classifyMcpToolForCollapse("github", "search_code")).toEqual({
isSearch: true,
isRead: false,
});
});
test("classifies Linear search_issues as search", () => {
expect(classifyMcpToolForCollapse("linear", "search_issues")).toEqual({
isSearch: true,
isRead: false,
});
});
test("classifies Datadog search_logs as search", () => {
expect(classifyMcpToolForCollapse("datadog", "search_logs")).toEqual({
isSearch: true,
isRead: false,
});
});
test("classifies Notion search as search", () => {
expect(classifyMcpToolForCollapse("notion", "search")).toEqual({
isSearch: true,
isRead: false,
});
});
test("classifies Brave brave_web_search as search", () => {
expect(classifyMcpToolForCollapse("brave-search", "brave_web_search")).toEqual({
isSearch: true,
isRead: false,
});
});
// Read tools
test("classifies Slack slack_read_channel as read", () => {
expect(classifyMcpToolForCollapse("slack", "slack_read_channel")).toEqual({
isSearch: false,
isRead: true,
});
});
test("classifies GitHub get_file_contents as read", () => {
expect(classifyMcpToolForCollapse("github", "get_file_contents")).toEqual({
isSearch: false,
isRead: true,
});
});
test("classifies Linear get_issue as read", () => {
expect(classifyMcpToolForCollapse("linear", "get_issue")).toEqual({
isSearch: false,
isRead: true,
});
});
test("classifies Filesystem read_file as read", () => {
expect(classifyMcpToolForCollapse("filesystem", "read_file")).toEqual({
isSearch: false,
isRead: true,
});
});
test("classifies GitHub list_commits as read", () => {
expect(classifyMcpToolForCollapse("github", "list_commits")).toEqual({
isSearch: false,
isRead: true,
});
});
test("classifies Slack slack_list_channels as read", () => {
expect(classifyMcpToolForCollapse("slack", "slack_list_channels")).toEqual({
isSearch: false,
isRead: true,
});
});
// Unknown tools
test("unknown tool returns { isSearch: false, isRead: false }", () => {
expect(classifyMcpToolForCollapse("unknown", "do_something")).toEqual({
isSearch: false,
isRead: false,
});
});
// normalize: camelCase -> snake_case
test("tool name with camelCase variant still matches after normalize", () => {
// searchCode -> search_code
expect(classifyMcpToolForCollapse("github", "searchCode")).toEqual({
isSearch: true,
isRead: false,
});
});
// normalize: kebab-case -> snake_case
test("tool name with kebab-case variant still matches after normalize", () => {
// search-code -> search_code
expect(classifyMcpToolForCollapse("github", "search-code")).toEqual({
isSearch: true,
isRead: false,
});
});
// Server name doesn't affect classification
test("server name parameter does not affect classification", () => {
const r1 = classifyMcpToolForCollapse("server-a", "search_code");
const r2 = classifyMcpToolForCollapse("server-b", "search_code");
expect(r1).toEqual(r2);
});
// Edge cases
test("empty tool name returns false/false", () => {
expect(classifyMcpToolForCollapse("server", "")).toEqual({
isSearch: false,
isRead: false,
});
});
// normalize lowercases, so SEARCH_CODE -> search_code -> matches
test("uppercase input normalizes to match", () => {
expect(classifyMcpToolForCollapse("github", "SEARCH_CODE")).toEqual({
isSearch: true,
isRead: false,
});
});
test("handles tool names with numbers", () => {
expect(classifyMcpToolForCollapse("server", "search2_things")).toEqual({
isSearch: false,
isRead: false,
});
});
});