mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-22 16:25:51 +00:00
style: 完成所有文件的lint
This commit is contained in:
@@ -1,97 +1,87 @@
|
||||
import { feature } from 'bun:bundle'
|
||||
import figures from 'figures'
|
||||
import * as React from 'react'
|
||||
import type { z } from 'zod/v4'
|
||||
import { ProgressBar } from '@anthropic/ink'
|
||||
import { MessageResponse } from 'src/components/MessageResponse.js'
|
||||
import {
|
||||
linkifyUrlsInText,
|
||||
OutputLine,
|
||||
} from 'src/components/shell/OutputLine.js'
|
||||
import { Ansi, Box, Text, stringWidth } from '@anthropic/ink'
|
||||
import { createHyperlink } from 'src/utils/hyperlink.js'
|
||||
import type { ToolProgressData } from 'src/Tool.js'
|
||||
import type { ProgressMessage } from 'src/types/message.js'
|
||||
import type { MCPProgress } from 'src/types/tools.js'
|
||||
import { formatNumber } from 'src/utils/format.js'
|
||||
import { feature } from 'bun:bundle';
|
||||
import figures from 'figures';
|
||||
import * as React from 'react';
|
||||
import type { z } from 'zod/v4';
|
||||
import { ProgressBar } from '@anthropic/ink';
|
||||
import { MessageResponse } from 'src/components/MessageResponse.js';
|
||||
import { linkifyUrlsInText, OutputLine } from 'src/components/shell/OutputLine.js';
|
||||
import { Ansi, Box, Text, stringWidth } from '@anthropic/ink';
|
||||
import { createHyperlink } from 'src/utils/hyperlink.js';
|
||||
import type { ToolProgressData } from 'src/Tool.js';
|
||||
import type { ProgressMessage } from 'src/types/message.js';
|
||||
import type { MCPProgress } from 'src/types/tools.js';
|
||||
import { formatNumber } from 'src/utils/format.js';
|
||||
|
||||
import {
|
||||
getContentSizeEstimate,
|
||||
type MCPToolResult,
|
||||
} from 'src/utils/mcpValidation.js'
|
||||
import { jsonParse, jsonStringify } from 'src/utils/slowOperations.js'
|
||||
import type { inputSchema } from './MCPTool.js'
|
||||
import { getContentSizeEstimate, type MCPToolResult } from 'src/utils/mcpValidation.js';
|
||||
import { jsonParse, jsonStringify } from 'src/utils/slowOperations.js';
|
||||
import type { inputSchema } from './MCPTool.js';
|
||||
|
||||
// Threshold for displaying warning about large MCP responses
|
||||
const MCP_OUTPUT_WARNING_THRESHOLD_TOKENS = 10_000
|
||||
const MCP_OUTPUT_WARNING_THRESHOLD_TOKENS = 10_000;
|
||||
|
||||
// In non-verbose mode, truncate individual input values to keep the header
|
||||
// compact. Matches BashTool's philosophy of showing enough to identify the
|
||||
// call without dumping the entire payload inline.
|
||||
const MAX_INPUT_VALUE_CHARS = 80
|
||||
const MAX_INPUT_VALUE_CHARS = 80;
|
||||
|
||||
// Max number of top-level keys before we fall back to raw JSON display.
|
||||
// Beyond this a flat k:v list is more noise than help.
|
||||
const MAX_FLAT_JSON_KEYS = 12
|
||||
const MAX_FLAT_JSON_KEYS = 12;
|
||||
|
||||
// Don't attempt flat-object parsing for large blobs.
|
||||
const MAX_FLAT_JSON_CHARS = 5_000
|
||||
const MAX_FLAT_JSON_CHARS = 5_000;
|
||||
|
||||
// Don't attempt to parse JSON blobs larger than this (perf safety).
|
||||
const MAX_JSON_PARSE_CHARS = 200_000
|
||||
const MAX_JSON_PARSE_CHARS = 200_000;
|
||||
|
||||
// A string value is "dominant text payload" if it has newlines or is
|
||||
// long enough that inline display would be worse than unwrapping.
|
||||
const UNWRAP_MIN_STRING_LEN = 200
|
||||
const UNWRAP_MIN_STRING_LEN = 200;
|
||||
|
||||
export function renderToolUseMessage(
|
||||
input: z.infer<ReturnType<typeof inputSchema>>,
|
||||
{ verbose }: { verbose: boolean },
|
||||
): React.ReactNode {
|
||||
if (Object.keys(input).length === 0) {
|
||||
return ''
|
||||
return '';
|
||||
}
|
||||
return Object.entries(input)
|
||||
.map(([key, value]) => {
|
||||
let rendered = jsonStringify(value)
|
||||
if (
|
||||
feature('MCP_RICH_OUTPUT') &&
|
||||
!verbose &&
|
||||
rendered.length > MAX_INPUT_VALUE_CHARS
|
||||
) {
|
||||
rendered = rendered.slice(0, MAX_INPUT_VALUE_CHARS).trimEnd() + '…'
|
||||
let rendered = jsonStringify(value);
|
||||
if (feature('MCP_RICH_OUTPUT') && !verbose && rendered.length > MAX_INPUT_VALUE_CHARS) {
|
||||
rendered = rendered.slice(0, MAX_INPUT_VALUE_CHARS).trimEnd() + '…';
|
||||
}
|
||||
return `${key}: ${rendered}`
|
||||
return `${key}: ${rendered}`;
|
||||
})
|
||||
.join(', ')
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
export function renderToolUseProgressMessage(
|
||||
progressMessagesForMessage: ProgressMessage<MCPProgress>[],
|
||||
): React.ReactNode {
|
||||
const lastProgress = progressMessagesForMessage.at(-1)
|
||||
const lastProgress = progressMessagesForMessage.at(-1);
|
||||
|
||||
if (!lastProgress?.data) {
|
||||
return (
|
||||
<MessageResponse height={1}>
|
||||
<Text dimColor>Running…</Text>
|
||||
</MessageResponse>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { progress, total, progressMessage } = lastProgress.data
|
||||
const { progress, total, progressMessage } = lastProgress.data;
|
||||
|
||||
if (progress === undefined) {
|
||||
return (
|
||||
<MessageResponse height={1}>
|
||||
<Text dimColor>Running…</Text>
|
||||
</MessageResponse>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (total !== undefined && total > 0) {
|
||||
const ratio = Math.min(1, Math.max(0, progress / total))
|
||||
const percentage = Math.round(ratio * 100)
|
||||
const ratio = Math.min(1, Math.max(0, progress / total));
|
||||
const percentage = Math.round(ratio * 100);
|
||||
return (
|
||||
<MessageResponse>
|
||||
<Box flexDirection="column">
|
||||
@@ -102,14 +92,14 @@ export function renderToolUseProgressMessage(
|
||||
</Box>
|
||||
</Box>
|
||||
</MessageResponse>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MessageResponse height={1}>
|
||||
<Text dimColor>{progressMessage ?? `Processing… ${progress}`}</Text>
|
||||
</MessageResponse>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function renderToolResultMessage(
|
||||
@@ -117,66 +107,57 @@ export function renderToolResultMessage(
|
||||
_progressMessagesForMessage: ProgressMessage<ToolProgressData>[],
|
||||
{ verbose, input }: { verbose: boolean; input?: unknown },
|
||||
): React.ReactNode {
|
||||
const mcpOutput = output as MCPToolResult
|
||||
const mcpOutput = output as MCPToolResult;
|
||||
|
||||
if (!verbose) {
|
||||
const slackSend = trySlackSendCompact(mcpOutput, input)
|
||||
const slackSend = trySlackSendCompact(mcpOutput, input);
|
||||
if (slackSend !== null) {
|
||||
return (
|
||||
<MessageResponse height={1}>
|
||||
<Text>
|
||||
Sent a message to{' '}
|
||||
<Ansi>{createHyperlink(slackSend.url, slackSend.channel)}</Ansi>
|
||||
Sent a message to <Ansi>{createHyperlink(slackSend.url, slackSend.channel)}</Ansi>
|
||||
</Text>
|
||||
</MessageResponse>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const estimatedTokens = getContentSizeEstimate(mcpOutput)
|
||||
const showWarning = estimatedTokens > MCP_OUTPUT_WARNING_THRESHOLD_TOKENS
|
||||
const estimatedTokens = getContentSizeEstimate(mcpOutput);
|
||||
const showWarning = estimatedTokens > MCP_OUTPUT_WARNING_THRESHOLD_TOKENS;
|
||||
const warningMessage = showWarning
|
||||
? `${figures.warning} Large MCP response (~${formatNumber(estimatedTokens)} tokens), this can fill up context quickly`
|
||||
: null
|
||||
: null;
|
||||
|
||||
let contentElement: React.ReactNode
|
||||
let contentElement: React.ReactNode;
|
||||
if (Array.isArray(mcpOutput)) {
|
||||
const contentBlocks = mcpOutput.map((item, i) => {
|
||||
if (item.type === 'image') {
|
||||
return (
|
||||
<Box
|
||||
key={i}
|
||||
justifyContent="space-between"
|
||||
overflowX="hidden"
|
||||
width="100%"
|
||||
>
|
||||
<Box key={i} justifyContent="space-between" overflowX="hidden" width="100%">
|
||||
<MessageResponse height={1}>
|
||||
<Text>[Image]</Text>
|
||||
</MessageResponse>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
// For text blocks and any other block types, extract text if available
|
||||
const textContent =
|
||||
item.type === 'text' &&
|
||||
'text' in item &&
|
||||
item.text !== null &&
|
||||
item.text !== undefined
|
||||
item.type === 'text' && 'text' in item && item.text !== null && item.text !== undefined
|
||||
? String(item.text)
|
||||
: ''
|
||||
: '';
|
||||
return feature('MCP_RICH_OUTPUT') ? (
|
||||
<MCPTextOutput key={i} content={textContent} verbose={verbose} />
|
||||
) : (
|
||||
<OutputLine key={i} content={textContent} verbose={verbose} />
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Wrap array content in a column layout
|
||||
contentElement = (
|
||||
<Box flexDirection="column" width="100%">
|
||||
{contentBlocks}
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
} else if (!mcpOutput) {
|
||||
contentElement = (
|
||||
<Box justifyContent="space-between" overflowX="hidden" width="100%">
|
||||
@@ -184,13 +165,13 @@ export function renderToolResultMessage(
|
||||
<Text dimColor>(No content)</Text>
|
||||
</MessageResponse>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
} else {
|
||||
contentElement = feature('MCP_RICH_OUTPUT') ? (
|
||||
<MCPTextOutput content={mcpOutput} verbose={verbose} />
|
||||
) : (
|
||||
<OutputLine content={mcpOutput} verbose={verbose} />
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (warningMessage) {
|
||||
@@ -201,10 +182,10 @@ export function renderToolResultMessage(
|
||||
</MessageResponse>
|
||||
{contentElement}
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return contentElement
|
||||
return contentElement;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -214,31 +195,23 @@ export function renderToolResultMessage(
|
||||
* 2. If JSON is a small flat-ish object, render as aligned key: value.
|
||||
* 3. Otherwise fall through to OutputLine (pretty-print + truncate).
|
||||
*/
|
||||
function MCPTextOutput({
|
||||
content,
|
||||
verbose,
|
||||
}: {
|
||||
content: string
|
||||
verbose: boolean
|
||||
}): React.ReactNode {
|
||||
const unwrapped = tryUnwrapTextPayload(content)
|
||||
function MCPTextOutput({ content, verbose }: { content: string; verbose: boolean }): React.ReactNode {
|
||||
const unwrapped = tryUnwrapTextPayload(content);
|
||||
if (unwrapped !== null) {
|
||||
return (
|
||||
<MessageResponse>
|
||||
<Box flexDirection="column">
|
||||
{unwrapped.extras.length > 0 && (
|
||||
<Text dimColor>
|
||||
{unwrapped.extras.map(([k, v]) => `${k}: ${v}`).join(' · ')}
|
||||
</Text>
|
||||
<Text dimColor>{unwrapped.extras.map(([k, v]) => `${k}: ${v}`).join(' · ')}</Text>
|
||||
)}
|
||||
<OutputLine content={unwrapped.body} verbose={verbose} linkifyUrls />
|
||||
</Box>
|
||||
</MessageResponse>
|
||||
)
|
||||
);
|
||||
}
|
||||
const flat = tryFlattenJson(content)
|
||||
const flat = tryFlattenJson(content);
|
||||
if (flat !== null) {
|
||||
const maxKeyWidth = Math.max(...flat.map(([k]) => stringWidth(k)))
|
||||
const maxKeyWidth = Math.max(...flat.map(([k]) => stringWidth(k)));
|
||||
return (
|
||||
<MessageResponse>
|
||||
<Box flexDirection="column">
|
||||
@@ -250,9 +223,9 @@ function MCPTextOutput({
|
||||
))}
|
||||
</Box>
|
||||
</MessageResponse>
|
||||
)
|
||||
);
|
||||
}
|
||||
return <OutputLine content={content} verbose={verbose} linkifyUrls />
|
||||
return <OutputLine content={content} verbose={verbose} linkifyUrls />;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -263,24 +236,24 @@ function parseJsonEntries(
|
||||
content: string,
|
||||
{ maxChars, maxKeys }: { maxChars: number; maxKeys: number },
|
||||
): [string, unknown][] | null {
|
||||
const trimmed = content.trim()
|
||||
const trimmed = content.trim();
|
||||
if (trimmed.length === 0 || trimmed.length > maxChars || trimmed[0] !== '{') {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
let parsed: unknown
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = jsonParse(trimmed)
|
||||
parsed = jsonParse(trimmed);
|
||||
} catch {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
const entries = Object.entries(parsed)
|
||||
const entries = Object.entries(parsed);
|
||||
if (entries.length === 0 || entries.length > maxKeys) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
return entries
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -292,27 +265,23 @@ export function tryFlattenJson(content: string): [string, string][] | null {
|
||||
const entries = parseJsonEntries(content, {
|
||||
maxChars: MAX_FLAT_JSON_CHARS,
|
||||
maxKeys: MAX_FLAT_JSON_KEYS,
|
||||
})
|
||||
if (entries === null) return null
|
||||
const result: [string, string][] = []
|
||||
});
|
||||
if (entries === null) return null;
|
||||
const result: [string, string][] = [];
|
||||
for (const [key, value] of entries) {
|
||||
if (typeof value === 'string') {
|
||||
result.push([key, value])
|
||||
} else if (
|
||||
value === null ||
|
||||
typeof value === 'number' ||
|
||||
typeof value === 'boolean'
|
||||
) {
|
||||
result.push([key, String(value)])
|
||||
result.push([key, value]);
|
||||
} else if (value === null || typeof value === 'number' || typeof value === 'boolean') {
|
||||
result.push([key, String(value)]);
|
||||
} else if (typeof value === 'object') {
|
||||
const compact = jsonStringify(value)
|
||||
if (compact.length > 120) return null
|
||||
result.push([key, compact])
|
||||
const compact = jsonStringify(value);
|
||||
if (compact.length > 120) return null;
|
||||
result.push([key, compact]);
|
||||
} else {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return result
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -321,46 +290,38 @@ export function tryFlattenJson(content: string): [string, string][] | null {
|
||||
* handles the common MCP pattern of {"messages":"line1\nline2..."} where
|
||||
* pretty-printing keeps \n escaped but we want real line breaks + truncation.
|
||||
*/
|
||||
export function tryUnwrapTextPayload(
|
||||
content: string,
|
||||
): { body: string; extras: [string, string][] } | null {
|
||||
export function tryUnwrapTextPayload(content: string): { body: string; extras: [string, string][] } | null {
|
||||
const entries = parseJsonEntries(content, {
|
||||
maxChars: MAX_JSON_PARSE_CHARS,
|
||||
maxKeys: 4,
|
||||
})
|
||||
if (entries === null) return null
|
||||
});
|
||||
if (entries === null) return null;
|
||||
// Find the one dominant string payload. Trim first: a trailing \n on a
|
||||
// short sibling (e.g. pagination hints) shouldn't make it "dominant".
|
||||
let body: string | null = null
|
||||
const extras: [string, string][] = []
|
||||
let body: string | null = null;
|
||||
const extras: [string, string][] = [];
|
||||
for (const [key, value] of entries) {
|
||||
if (typeof value === 'string') {
|
||||
const t = value.trimEnd()
|
||||
const isDominant =
|
||||
t.length > UNWRAP_MIN_STRING_LEN || (t.includes('\n') && t.length > 50)
|
||||
const t = value.trimEnd();
|
||||
const isDominant = t.length > UNWRAP_MIN_STRING_LEN || (t.includes('\n') && t.length > 50);
|
||||
if (isDominant) {
|
||||
if (body !== null) return null // two big strings — ambiguous
|
||||
body = t
|
||||
continue
|
||||
if (body !== null) return null; // two big strings — ambiguous
|
||||
body = t;
|
||||
continue;
|
||||
}
|
||||
if (t.length > 150) return null
|
||||
extras.push([key, t.replace(/\s+/g, ' ')])
|
||||
} else if (
|
||||
value === null ||
|
||||
typeof value === 'number' ||
|
||||
typeof value === 'boolean'
|
||||
) {
|
||||
extras.push([key, String(value)])
|
||||
if (t.length > 150) return null;
|
||||
extras.push([key, t.replace(/\s+/g, ' ')]);
|
||||
} else if (value === null || typeof value === 'number' || typeof value === 'boolean') {
|
||||
extras.push([key, String(value)]);
|
||||
} else {
|
||||
return null // nested object/array — use flat or pretty-print path
|
||||
return null; // nested object/array — use flat or pretty-print path
|
||||
}
|
||||
}
|
||||
if (body === null) return null
|
||||
return { body, extras }
|
||||
if (body === null) return null;
|
||||
return { body, extras };
|
||||
}
|
||||
|
||||
const SLACK_ARCHIVES_RE =
|
||||
/^https:\/\/[a-z0-9-]+\.slack\.com\/archives\/([A-Z0-9]+)\/p\d+$/
|
||||
const SLACK_ARCHIVES_RE = /^https:\/\/[a-z0-9-]+\.slack\.com\/archives\/([A-Z0-9]+)\/p\d+$/;
|
||||
|
||||
/**
|
||||
* Detect a Slack send-message result and return a compact {channel, url} pair.
|
||||
@@ -373,23 +334,23 @@ export function trySlackSendCompact(
|
||||
output: string | MCPToolResult,
|
||||
input: unknown,
|
||||
): { channel: string; url: string } | null {
|
||||
let text: unknown = output
|
||||
let text: unknown = output;
|
||||
if (Array.isArray(output)) {
|
||||
const block = output.find(b => b.type === 'text')
|
||||
text = block && 'text' in block ? block.text : undefined
|
||||
const block = output.find(b => b.type === 'text');
|
||||
text = block && 'text' in block ? block.text : undefined;
|
||||
}
|
||||
if (typeof text !== 'string' || !text.includes('"message_link"')) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
const entries = parseJsonEntries(text, { maxChars: 2000, maxKeys: 6 })
|
||||
const url = entries?.find(([k]) => k === 'message_link')?.[1]
|
||||
if (typeof url !== 'string') return null
|
||||
const m = SLACK_ARCHIVES_RE.exec(url)
|
||||
if (!m) return null
|
||||
const entries = parseJsonEntries(text, { maxChars: 2000, maxKeys: 6 });
|
||||
const url = entries?.find(([k]) => k === 'message_link')?.[1];
|
||||
if (typeof url !== 'string') return null;
|
||||
const m = SLACK_ARCHIVES_RE.exec(url);
|
||||
if (!m) return null;
|
||||
|
||||
const inp = input as { channel_id?: unknown; channel?: unknown } | undefined
|
||||
const raw = inp?.channel_id ?? inp?.channel ?? m[1]
|
||||
const label = typeof raw === 'string' && raw ? raw : 'slack'
|
||||
return { channel: label.startsWith('#') ? label : `#${label}`, url }
|
||||
const inp = input as { channel_id?: unknown; channel?: unknown } | undefined;
|
||||
const raw = inp?.channel_id ?? inp?.channel ?? m[1];
|
||||
const label = typeof raw === 'string' && raw ? raw : 'slack';
|
||||
return { channel: label.startsWith('#') ? label : `#${label}`, url };
|
||||
}
|
||||
|
||||
@@ -1,146 +1,148 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { classifyMcpToolForCollapse } from "../classifyForCollapse";
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
import { classifyMcpToolForCollapse } from '../classifyForCollapse'
|
||||
|
||||
describe("classifyMcpToolForCollapse", () => {
|
||||
describe('classifyMcpToolForCollapse', () => {
|
||||
// Search tools
|
||||
test("classifies Slack slack_search_public as search", () => {
|
||||
expect(classifyMcpToolForCollapse("slack", "slack_search_public")).toEqual({
|
||||
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({
|
||||
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({
|
||||
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({
|
||||
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({
|
||||
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({
|
||||
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({
|
||||
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({
|
||||
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({
|
||||
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({
|
||||
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({
|
||||
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({
|
||||
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({
|
||||
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", () => {
|
||||
test('tool name with camelCase variant still matches after normalize', () => {
|
||||
// searchCode -> search_code
|
||||
expect(classifyMcpToolForCollapse("github", "searchCode")).toEqual({
|
||||
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", () => {
|
||||
test('tool name with kebab-case variant still matches after normalize', () => {
|
||||
// search-code -> search_code
|
||||
expect(classifyMcpToolForCollapse("github", "search-code")).toEqual({
|
||||
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);
|
||||
});
|
||||
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({
|
||||
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({
|
||||
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({
|
||||
test('handles tool names with numbers', () => {
|
||||
expect(classifyMcpToolForCollapse('server', 'search2_things')).toEqual({
|
||||
isSearch: false,
|
||||
isRead: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user