Skip to content

Commit 97f4d3c

Browse files
committed
test: add missing semantic-embedding mocks to test files
Add vi.mock for semantic-embedding to prevent test timeouts: - w102-missing-examples.test.ts - w108-hidden-side-effects.test.ts - w109-output-not-reusable.test.ts - w111-description-quality.test.ts - e100-missing-output-schema.test.ts - e104-param-not-in-description.test.ts - e107-circular-dependency.test.ts - e109-non-serializable.test.ts All 738 tests pass
1 parent a218932 commit 97f4d3c

11 files changed

+2745
-0
lines changed
Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
/**
2+
* Tests for E112: Security - Sensitive Parameter Detection rule.
3+
*/
4+
5+
import { describe, it, expect, beforeEach, vi } from 'vitest';
6+
import { analyseTools } from '../../analyser';
7+
import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
8+
import { buildIndexesFromTools } from '../__test-helpers__';
9+
10+
// Mock dependencies
11+
vi.mock('../../loader', () => ({ loadMCPTools: vi.fn() }));
12+
vi.mock('../../normalizer', () => ({ normalizeTools: vi.fn() }));
13+
vi.mock('../../indexer', () => ({ buildIndexes: vi.fn() }));
14+
vi.mock('../../dependencies', () => ({ inferDependencies: vi.fn() }));
15+
vi.mock('../../semantic-embedding', () => ({
16+
initializeConceptEmbeddings: vi.fn().mockResolvedValue(undefined),
17+
isConceptMatch: vi.fn(),
18+
}));
19+
vi.mock('@/utils/logger', () => ({
20+
log: {
21+
info: vi.fn(),
22+
warn: vi.fn(),
23+
warning: vi.fn(),
24+
error: vi.fn(),
25+
debug: vi.fn(),
26+
success: vi.fn(),
27+
plain: vi.fn(),
28+
blank: vi.fn(),
29+
heading: vi.fn(),
30+
label: vi.fn(),
31+
value: vi.fn(),
32+
labelValue: vi.fn(),
33+
numberedItem: vi.fn(),
34+
checkmark: vi.fn(),
35+
xmark: vi.fn(),
36+
warnSymbol: vi.fn(),
37+
tick: vi.fn(() => '✓'),
38+
cross: vi.fn(() => '✗'),
39+
styleText: vi.fn(text => text),
40+
},
41+
}));
42+
43+
describe('E112: Security - Sensitive Parameter Detection', () => {
44+
let mockClient: Client;
45+
46+
beforeEach(() => {
47+
vi.clearAllMocks();
48+
mockClient = {} as Client;
49+
});
50+
51+
it('should detect sensitive parameters like password', async () => {
52+
const { loadMCPTools } = await import('../../loader');
53+
const { normalizeTools } = await import('../../normalizer');
54+
const { buildIndexes } = await import('../../indexer');
55+
const { inferDependencies } = await import('../../dependencies');
56+
const { isConceptMatch } = await import('../../semantic-embedding');
57+
58+
vi.mocked(loadMCPTools).mockResolvedValue([
59+
{
60+
name: 'authenticate_user',
61+
description: 'Authenticate user with credentials',
62+
inputSchema: {
63+
type: 'object',
64+
properties: {
65+
username: { type: 'string' },
66+
password: { type: 'string', description: 'User password' },
67+
},
68+
required: ['username', 'password'],
69+
},
70+
outputSchema: {
71+
type: 'object',
72+
properties: {
73+
token: { type: 'string' },
74+
},
75+
},
76+
},
77+
]);
78+
79+
// Mock isConceptMatch to return true only for sensitive parameters
80+
vi.mocked(isConceptMatch).mockImplementation(
81+
(embedding, category, _threshold) => {
82+
// Only return true for SENSITIVE category AND when embedding matches password
83+
if (category !== 'SENSITIVE') return false;
84+
// password embedding is [0.4, 0.5, 0.6]
85+
return (
86+
embedding?.length === 3 &&
87+
embedding[0] === 0.4 &&
88+
embedding[1] === 0.5 &&
89+
embedding[2] === 0.6
90+
);
91+
}
92+
);
93+
94+
const normalizedTools = [
95+
{
96+
name: 'authenticate_user',
97+
description: 'Authenticate user with credentials',
98+
inputs: [
99+
{
100+
tool: 'authenticate_user',
101+
name: 'username',
102+
type: 'string',
103+
required: true,
104+
},
105+
{
106+
tool: 'authenticate_user',
107+
name: 'password',
108+
type: 'string',
109+
required: true,
110+
description: 'User password',
111+
},
112+
],
113+
outputs: [
114+
{
115+
tool: 'authenticate_user',
116+
name: 'token',
117+
type: 'string',
118+
required: false,
119+
},
120+
],
121+
descriptionTokens: new Set(['authenticate', 'user', 'credentials']),
122+
inputEmbeddings: new Map([
123+
['username', [0.1, 0.2, 0.3]],
124+
['password', [0.4, 0.5, 0.6]],
125+
]),
126+
},
127+
];
128+
129+
vi.mocked(normalizeTools).mockResolvedValue(normalizedTools);
130+
vi.mocked(buildIndexes).mockReturnValue(
131+
buildIndexesFromTools(normalizedTools)
132+
);
133+
vi.mocked(inferDependencies).mockReturnValue([]);
134+
135+
const result = await analyseTools(mockClient);
136+
137+
const e112Errors = result.errors.filter(e => e.code === 'E112');
138+
expect(e112Errors.length).toBeGreaterThan(0);
139+
expect(e112Errors[0]?.tool).toBe('authenticate_user');
140+
expect(e112Errors[0]?.field).toBe('password');
141+
expect(e112Errors[0]?.message).toContain('Security risk');
142+
expect(e112Errors[0]?.message).toContain('sensitive data');
143+
});
144+
145+
it('should detect sensitive parameters like apiKey', async () => {
146+
const { loadMCPTools } = await import('../../loader');
147+
const { normalizeTools } = await import('../../normalizer');
148+
const { buildIndexes } = await import('../../indexer');
149+
const { inferDependencies } = await import('../../dependencies');
150+
const { isConceptMatch } = await import('../../semantic-embedding');
151+
152+
vi.mocked(loadMCPTools).mockResolvedValue([
153+
{
154+
name: 'call_external_api',
155+
description: 'Call external API with authentication',
156+
inputSchema: {
157+
type: 'object',
158+
properties: {
159+
endpoint: { type: 'string' },
160+
apiKey: {
161+
type: 'string',
162+
description: 'API key for authentication',
163+
},
164+
},
165+
required: ['endpoint', 'apiKey'],
166+
},
167+
outputSchema: {
168+
type: 'object',
169+
properties: {
170+
result: { type: 'string' },
171+
},
172+
},
173+
},
174+
]);
175+
176+
// Mock isConceptMatch to return true only for sensitive parameters
177+
vi.mocked(isConceptMatch).mockImplementation(
178+
(embedding, category, _threshold) => {
179+
// Only return true for SENSITIVE category AND when embedding matches apiKey
180+
if (category !== 'SENSITIVE') return false;
181+
// apiKey embedding is [0.4, 0.5, 0.6]
182+
return (
183+
embedding?.length === 3 &&
184+
embedding[0] === 0.4 &&
185+
embedding[1] === 0.5 &&
186+
embedding[2] === 0.6
187+
);
188+
}
189+
);
190+
191+
const normalizedTools = [
192+
{
193+
name: 'call_external_api',
194+
description: 'Call external API with authentication',
195+
inputs: [
196+
{
197+
tool: 'call_external_api',
198+
name: 'endpoint',
199+
type: 'string',
200+
required: true,
201+
},
202+
{
203+
tool: 'call_external_api',
204+
name: 'apiKey',
205+
type: 'string',
206+
required: true,
207+
description: 'API key for authentication',
208+
},
209+
],
210+
outputs: [
211+
{
212+
tool: 'call_external_api',
213+
name: 'result',
214+
type: 'string',
215+
required: false,
216+
},
217+
],
218+
descriptionTokens: new Set(['call', 'external', 'api']),
219+
inputEmbeddings: new Map([
220+
['endpoint', [0.1, 0.2, 0.3]],
221+
['apiKey', [0.4, 0.5, 0.6]],
222+
]),
223+
},
224+
];
225+
226+
vi.mocked(normalizeTools).mockResolvedValue(normalizedTools);
227+
vi.mocked(buildIndexes).mockReturnValue(
228+
buildIndexesFromTools(normalizedTools)
229+
);
230+
vi.mocked(inferDependencies).mockReturnValue([]);
231+
232+
const result = await analyseTools(mockClient);
233+
234+
const e112Errors = result.errors.filter(e => e.code === 'E112');
235+
expect(e112Errors.length).toBeGreaterThan(0);
236+
expect(e112Errors[0]?.field).toBe('apiKey');
237+
});
238+
239+
it('should pass when parameters are not sensitive', async () => {
240+
const { loadMCPTools } = await import('../../loader');
241+
const { normalizeTools } = await import('../../normalizer');
242+
const { buildIndexes } = await import('../../indexer');
243+
const { inferDependencies } = await import('../../dependencies');
244+
const { isConceptMatch } = await import('../../semantic-embedding');
245+
246+
vi.mocked(loadMCPTools).mockResolvedValue([
247+
{
248+
name: 'get_user',
249+
description: 'Get user information',
250+
inputSchema: {
251+
type: 'object',
252+
properties: {
253+
userId: { type: 'string', description: 'User ID' },
254+
includeDetails: { type: 'boolean', description: 'Include details' },
255+
},
256+
},
257+
outputSchema: {
258+
type: 'object',
259+
properties: {
260+
user: { type: 'object' },
261+
},
262+
},
263+
},
264+
]);
265+
266+
// Mock isConceptMatch to return false (not sensitive)
267+
vi.mocked(isConceptMatch).mockReturnValue(false);
268+
269+
const normalizedTools = [
270+
{
271+
name: 'get_user',
272+
description: 'Get user information',
273+
inputs: [
274+
{
275+
tool: 'get_user',
276+
name: 'userId',
277+
type: 'string',
278+
required: true,
279+
description: 'User ID',
280+
},
281+
{
282+
tool: 'get_user',
283+
name: 'includeDetails',
284+
type: 'boolean',
285+
required: false,
286+
description: 'Include details',
287+
},
288+
],
289+
outputs: [
290+
{
291+
tool: 'get_user',
292+
name: 'user',
293+
type: 'object',
294+
required: false,
295+
},
296+
],
297+
descriptionTokens: new Set(['get', 'user', 'information']),
298+
inputEmbeddings: new Map([
299+
['userId', [0.1, 0.2, 0.3]],
300+
['includeDetails', [0.4, 0.5, 0.6]],
301+
]),
302+
},
303+
];
304+
305+
vi.mocked(normalizeTools).mockResolvedValue(normalizedTools);
306+
vi.mocked(buildIndexes).mockReturnValue(
307+
buildIndexesFromTools(normalizedTools)
308+
);
309+
vi.mocked(inferDependencies).mockReturnValue([]);
310+
311+
const result = await analyseTools(mockClient);
312+
313+
const e112Errors = result.errors.filter(e => e.code === 'E112');
314+
expect(e112Errors).toHaveLength(0);
315+
});
316+
});

0 commit comments

Comments
 (0)