Skip to content

Commit 98df118

Browse files
Migrate to vitest from jest (#1074)
Co-authored-by: Felix Weinberger <3823880+felixweinberger@users.noreply.github.com> Co-authored-by: Felix Weinberger <fweinberger@anthropic.com>
1 parent 324d471 commit 98df118

30 files changed

+1713
-3434
lines changed

jest.config.js

Lines changed: 0 additions & 14 deletions
This file was deleted.

package-lock.json

Lines changed: 1313 additions & 3046 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@
7171
"lint": "eslint src/ && prettier --check .",
7272
"lint:fix": "eslint src/ --fix && prettier --write .",
7373
"check": "npm run typecheck && npm run lint",
74-
"test": "jest",
74+
"test": "vitest run",
75+
"test:watch": "vitest",
7576
"start": "npm run server",
7677
"server": "tsx watch --clear-screen=false scripts/cli.ts server",
7778
"client": "tsx scripts/cli.ts client"
@@ -102,27 +103,24 @@
102103
"devDependencies": {
103104
"@cfworker/json-schema": "^4.1.1",
104105
"@eslint/js": "^9.8.0",
105-
"@jest-mock/express": "^3.0.0",
106106
"@types/content-type": "^1.1.8",
107107
"@types/cors": "^2.8.17",
108108
"@types/cross-spawn": "^6.0.6",
109109
"@types/eslint__js": "^8.42.3",
110110
"@types/eventsource": "^1.1.15",
111111
"@types/express": "^5.0.0",
112-
"@types/jest": "^29.5.12",
113112
"@types/node": "^22.0.2",
114113
"@types/supertest": "^6.0.2",
115114
"@types/ws": "^8.5.12",
116115
"@typescript/native-preview": "^7.0.0-dev.20251103.1",
117116
"eslint": "^9.8.0",
118117
"eslint-config-prettier": "^10.1.8",
119-
"jest": "^29.7.0",
120118
"prettier": "3.6.2",
121119
"supertest": "^7.0.0",
122-
"ts-jest": "^29.2.4",
123120
"tsx": "^4.16.5",
124121
"typescript": "^5.5.4",
125122
"typescript-eslint": "^8.0.0",
123+
"vitest": "^4.0.8",
126124
"ws": "^8.18.0"
127125
},
128126
"resolutions": {

src/client/auth.test.ts

Lines changed: 83 additions & 72 deletions
Large diffs are not rendered by default.

src/client/cross-spawn.test.ts

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,34 @@ import { StdioClientTransport, getDefaultEnvironment } from './stdio.js';
22
import spawn from 'cross-spawn';
33
import { JSONRPCMessage } from '../types.js';
44
import { ChildProcess } from 'node:child_process';
5+
import { Mock, MockedFunction } from 'vitest';
56

67
// mock cross-spawn
7-
jest.mock('cross-spawn');
8-
const mockSpawn = spawn as jest.MockedFunction<typeof spawn>;
8+
vi.mock('cross-spawn');
9+
const mockSpawn = spawn as unknown as MockedFunction<typeof spawn>;
910

1011
describe('StdioClientTransport using cross-spawn', () => {
1112
beforeEach(() => {
1213
// mock cross-spawn's return value
1314
mockSpawn.mockImplementation(() => {
1415
const mockProcess: {
15-
on: jest.Mock;
16-
stdin?: { on: jest.Mock; write: jest.Mock };
17-
stdout?: { on: jest.Mock };
16+
on: Mock;
17+
stdin?: { on: Mock; write: Mock };
18+
stdout?: { on: Mock };
1819
stderr?: null;
1920
} = {
20-
on: jest.fn((event: string, callback: () => void) => {
21+
on: vi.fn((event: string, callback: () => void) => {
2122
if (event === 'spawn') {
2223
callback();
2324
}
2425
return mockProcess;
2526
}),
2627
stdin: {
27-
on: jest.fn(),
28-
write: jest.fn().mockReturnValue(true)
28+
on: vi.fn(),
29+
write: vi.fn().mockReturnValue(true)
2930
},
3031
stdout: {
31-
on: jest.fn()
32+
on: vi.fn()
3233
},
3334
stderr: null
3435
};
@@ -37,7 +38,7 @@ describe('StdioClientTransport using cross-spawn', () => {
3738
});
3839

3940
afterEach(() => {
40-
jest.clearAllMocks();
41+
vi.clearAllMocks();
4142
});
4243

4344
test('should call cross-spawn correctly', async () => {
@@ -105,30 +106,30 @@ describe('StdioClientTransport using cross-spawn', () => {
105106

106107
// get the mock process object
107108
const mockProcess: {
108-
on: jest.Mock;
109+
on: Mock;
109110
stdin: {
110-
on: jest.Mock;
111-
write: jest.Mock;
112-
once: jest.Mock;
111+
on: Mock;
112+
write: Mock;
113+
once: Mock;
113114
};
114115
stdout: {
115-
on: jest.Mock;
116+
on: Mock;
116117
};
117118
stderr: null;
118119
} = {
119-
on: jest.fn((event: string, callback: () => void) => {
120+
on: vi.fn((event: string, callback: () => void) => {
120121
if (event === 'spawn') {
121122
callback();
122123
}
123124
return mockProcess;
124125
}),
125126
stdin: {
126-
on: jest.fn(),
127-
write: jest.fn().mockReturnValue(true),
128-
once: jest.fn()
127+
on: vi.fn(),
128+
write: vi.fn().mockReturnValue(true),
129+
once: vi.fn()
129130
},
130131
stdout: {
131-
on: jest.fn()
132+
on: vi.fn()
132133
},
133134
stderr: null
134135
};

src/client/index.test.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ import { InMemoryTransport } from '../inMemory.js';
2727
*/
2828
test('should initialize with matching protocol version', async () => {
2929
const clientTransport: Transport = {
30-
start: jest.fn().mockResolvedValue(undefined),
31-
close: jest.fn().mockResolvedValue(undefined),
32-
send: jest.fn().mockImplementation(message => {
30+
start: vi.fn().mockResolvedValue(undefined),
31+
close: vi.fn().mockResolvedValue(undefined),
32+
send: vi.fn().mockImplementation(message => {
3333
if (message.method === 'initialize') {
3434
clientTransport.onmessage?.({
3535
jsonrpc: '2.0',
@@ -86,9 +86,9 @@ test('should initialize with matching protocol version', async () => {
8686
test('should initialize with supported older protocol version', async () => {
8787
const OLD_VERSION = SUPPORTED_PROTOCOL_VERSIONS[1];
8888
const clientTransport: Transport = {
89-
start: jest.fn().mockResolvedValue(undefined),
90-
close: jest.fn().mockResolvedValue(undefined),
91-
send: jest.fn().mockImplementation(message => {
89+
start: vi.fn().mockResolvedValue(undefined),
90+
close: vi.fn().mockResolvedValue(undefined),
91+
send: vi.fn().mockImplementation(message => {
9292
if (message.method === 'initialize') {
9393
clientTransport.onmessage?.({
9494
jsonrpc: '2.0',
@@ -136,9 +136,9 @@ test('should initialize with supported older protocol version', async () => {
136136
*/
137137
test('should reject unsupported protocol version', async () => {
138138
const clientTransport: Transport = {
139-
start: jest.fn().mockResolvedValue(undefined),
140-
close: jest.fn().mockResolvedValue(undefined),
141-
send: jest.fn().mockImplementation(message => {
139+
start: vi.fn().mockResolvedValue(undefined),
140+
close: vi.fn().mockResolvedValue(undefined),
141+
send: vi.fn().mockImplementation(message => {
142142
if (message.method === 'initialize') {
143143
clientTransport.onmessage?.({
144144
jsonrpc: '2.0',

src/client/middleware.test.ts

Lines changed: 48 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,28 @@
11
import { withOAuth, withLogging, applyMiddlewares, createMiddleware } from './middleware.js';
22
import { OAuthClientProvider } from './auth.js';
33
import { FetchLike } from '../shared/transport.js';
4+
import { MockInstance, Mocked, MockedFunction } from 'vitest';
45

5-
jest.mock('../client/auth.js', () => {
6-
const actual = jest.requireActual('../client/auth.js');
6+
vi.mock('../client/auth.js', async () => {
7+
const actual = await vi.importActual<typeof import('../client/auth.js')>('../client/auth.js');
78
return {
89
...actual,
9-
auth: jest.fn(),
10-
extractWWWAuthenticateParams: jest.fn()
10+
auth: vi.fn(),
11+
extractWWWAuthenticateParams: vi.fn()
1112
};
1213
});
1314

1415
import { auth, extractWWWAuthenticateParams } from './auth.js';
1516

16-
const mockAuth = auth as jest.MockedFunction<typeof auth>;
17-
const mockExtractWWWAuthenticateParams = extractWWWAuthenticateParams as jest.MockedFunction<typeof extractWWWAuthenticateParams>;
17+
const mockAuth = auth as MockedFunction<typeof auth>;
18+
const mockExtractWWWAuthenticateParams = extractWWWAuthenticateParams as MockedFunction<typeof extractWWWAuthenticateParams>;
1819

1920
describe('withOAuth', () => {
20-
let mockProvider: jest.Mocked<OAuthClientProvider>;
21-
let mockFetch: jest.MockedFunction<FetchLike>;
21+
let mockProvider: Mocked<OAuthClientProvider>;
22+
let mockFetch: MockedFunction<FetchLike>;
2223

2324
beforeEach(() => {
24-
jest.clearAllMocks();
25+
vi.clearAllMocks();
2526

2627
mockProvider = {
2728
get redirectUrl() {
@@ -30,16 +31,16 @@ describe('withOAuth', () => {
3031
get clientMetadata() {
3132
return { redirect_uris: ['http://localhost/callback'] };
3233
},
33-
tokens: jest.fn(),
34-
saveTokens: jest.fn(),
35-
clientInformation: jest.fn(),
36-
redirectToAuthorization: jest.fn(),
37-
saveCodeVerifier: jest.fn(),
38-
codeVerifier: jest.fn(),
39-
invalidateCredentials: jest.fn()
34+
tokens: vi.fn(),
35+
saveTokens: vi.fn(),
36+
clientInformation: vi.fn(),
37+
redirectToAuthorization: vi.fn(),
38+
saveCodeVerifier: vi.fn(),
39+
codeVerifier: vi.fn(),
40+
invalidateCredentials: vi.fn()
4041
};
4142

42-
mockFetch = jest.fn();
43+
mockFetch = vi.fn();
4344
});
4445

4546
it('should add Authorization header when tokens are available (with explicit baseUrl)', async () => {
@@ -371,8 +372,8 @@ describe('withOAuth', () => {
371372
});
372373

373374
describe('withLogging', () => {
374-
let mockFetch: jest.MockedFunction<FetchLike>;
375-
let mockLogger: jest.MockedFunction<
375+
let mockFetch: MockedFunction<FetchLike>;
376+
let mockLogger: MockedFunction<
376377
(input: {
377378
method: string;
378379
url: string | URL;
@@ -384,17 +385,17 @@ describe('withLogging', () => {
384385
error?: Error;
385386
}) => void
386387
>;
387-
let consoleErrorSpy: jest.SpyInstance;
388-
let consoleLogSpy: jest.SpyInstance;
388+
let consoleErrorSpy: MockInstance;
389+
let consoleLogSpy: MockInstance;
389390

390391
beforeEach(() => {
391-
jest.clearAllMocks();
392+
vi.clearAllMocks();
392393

393-
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
394-
consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
394+
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
395+
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
395396

396-
mockFetch = jest.fn();
397-
mockLogger = jest.fn();
397+
mockFetch = vi.fn();
398+
mockLogger = vi.fn();
398399
});
399400

400401
afterEach(() => {
@@ -614,11 +615,11 @@ describe('withLogging', () => {
614615
});
615616

616617
describe('applyMiddleware', () => {
617-
let mockFetch: jest.MockedFunction<FetchLike>;
618+
let mockFetch: MockedFunction<FetchLike>;
618619

619620
beforeEach(() => {
620-
jest.clearAllMocks();
621-
mockFetch = jest.fn();
621+
vi.clearAllMocks();
622+
mockFetch = vi.fn();
622623
});
623624

624625
it('should compose no middleware correctly', () => {
@@ -703,7 +704,7 @@ describe('applyMiddleware', () => {
703704
};
704705

705706
// Use custom logger to avoid console output
706-
const mockLogger = jest.fn();
707+
const mockLogger = vi.fn();
707708
const composedFetch = applyMiddlewares(oauthMiddleware, withLogging({ logger: mockLogger, statusLevel: 0 }))(mockFetch);
708709

709710
await composedFetch('https://api.example.com/data');
@@ -743,11 +744,11 @@ describe('applyMiddleware', () => {
743744
});
744745

745746
describe('Integration Tests', () => {
746-
let mockProvider: jest.Mocked<OAuthClientProvider>;
747-
let mockFetch: jest.MockedFunction<FetchLike>;
747+
let mockProvider: Mocked<OAuthClientProvider>;
748+
let mockFetch: MockedFunction<FetchLike>;
748749

749750
beforeEach(() => {
750-
jest.clearAllMocks();
751+
vi.clearAllMocks();
751752

752753
mockProvider = {
753754
get redirectUrl() {
@@ -756,16 +757,16 @@ describe('Integration Tests', () => {
756757
get clientMetadata() {
757758
return { redirect_uris: ['http://localhost/callback'] };
758759
},
759-
tokens: jest.fn(),
760-
saveTokens: jest.fn(),
761-
clientInformation: jest.fn(),
762-
redirectToAuthorization: jest.fn(),
763-
saveCodeVerifier: jest.fn(),
764-
codeVerifier: jest.fn(),
765-
invalidateCredentials: jest.fn()
760+
tokens: vi.fn(),
761+
saveTokens: vi.fn(),
762+
clientInformation: vi.fn(),
763+
redirectToAuthorization: vi.fn(),
764+
saveCodeVerifier: vi.fn(),
765+
codeVerifier: vi.fn(),
766+
invalidateCredentials: vi.fn()
766767
};
767768

768-
mockFetch = jest.fn();
769+
mockFetch = vi.fn();
769770
});
770771

771772
it('should work with SSE transport pattern', async () => {
@@ -783,7 +784,7 @@ describe('Integration Tests', () => {
783784
mockFetch.mockResolvedValue(response);
784785

785786
// Use custom logger to avoid console output
786-
const mockLogger = jest.fn();
787+
const mockLogger = vi.fn();
787788
const enhancedFetch = applyMiddlewares(
788789
withOAuth(mockProvider as OAuthClientProvider, 'https://mcp-server.example.com'),
789790
withLogging({ logger: mockLogger, statusLevel: 400 }) // Only log errors
@@ -830,7 +831,7 @@ describe('Integration Tests', () => {
830831
mockFetch.mockResolvedValue(response);
831832

832833
// Use custom logger to avoid console output
833-
const mockLogger = jest.fn();
834+
const mockLogger = vi.fn();
834835
const enhancedFetch = applyMiddlewares(
835836
withOAuth(mockProvider as OAuthClientProvider, 'https://streamable-server.example.com'),
836837
withLogging({
@@ -891,7 +892,7 @@ describe('Integration Tests', () => {
891892
mockAuth.mockResolvedValue('AUTHORIZED');
892893

893894
// Use custom logger to avoid console output
894-
const mockLogger = jest.fn();
895+
const mockLogger = vi.fn();
895896
const enhancedFetch = applyMiddlewares(
896897
withOAuth(mockProvider as OAuthClientProvider, 'https://mcp-server.example.com'),
897898
withLogging({ logger: mockLogger, statusLevel: 0 })
@@ -914,11 +915,11 @@ describe('Integration Tests', () => {
914915
});
915916

916917
describe('createMiddleware', () => {
917-
let mockFetch: jest.MockedFunction<FetchLike>;
918+
let mockFetch: MockedFunction<FetchLike>;
918919

919920
beforeEach(() => {
920-
jest.clearAllMocks();
921-
mockFetch = jest.fn();
921+
vi.clearAllMocks();
922+
mockFetch = vi.fn();
922923
});
923924

924925
it('should create middleware with cleaner syntax', async () => {

0 commit comments

Comments
 (0)