Skip to content

Commit b58b88d

Browse files
committed
refactor(test): swap sinon for vitest's native vi.fn / vi.spyOn
Replaces ~1,500 sinon call sites across 45 test files with vitest's native mocking API. Drops `sinon` + `@types/sinon` devDeps from pure-vitest packages. Hybrid packages (sdk, rebalancer, cli, infra, svm-sdk, tron-sdk, relayer) keep sinon for hardhat/e2e paths that stay on mocha. ## Mapping ### Stubs / spies - `sinon.stub()` → `vi.fn()` - `sinon.stub(obj, 'm')` → `vi.spyOn(obj, 'm').mockImplementation(...)` - `sinon.spy(obj, 'm')` → `vi.spyOn(obj, 'm')` - `.returns(x)` → `.mockReturnValue(x)` - `.resolves(x)` → `.mockResolvedValue(x)` (argument now required — `.mockResolvedValue(undefined)` for void-returning stubs) - `.rejects(err)` → `.mockRejectedValue(err)` - `.callsFake(fn)` → `.mockImplementation(fn)` - `.onFirstCall().X()` / `.onSecondCall().X()` chains → `.mockXOnce(...).mockXOnce(...)` ### Assertions - `sinon.assert.calledOnce(s)` → `expect(s).toHaveBeenCalledOnce()` - `sinon.assert.calledTwice(s)` → `expect(s).toHaveBeenCalledTimes(2)` - `sinon.assert.callCount(s, n)` → `expect(s).toHaveBeenCalledTimes(n)` - `sinon.assert.calledWith(s, ...args)` → `expect(s).toHaveBeenCalledWith(...args)` - `sinon.assert.calledOnceWithExactly(s, ...a)` → `expect(s).toHaveBeenCalledExactlyOnceWith(...a)` - `sinon.assert.calledWithMatch(s, obj)` → `expect(s).toHaveBeenCalledWith(expect.objectContaining(obj))` - `sinon.assert.notCalled(s)` → `expect(s).not.toHaveBeenCalled()` - `s.callCount` → `s.mock.calls.length` - `s.firstCall.args[n]` / `s.getCall(n).args[m]` → `s.mock.calls[n][m]` - `s.returnValues[n]` → `s.mock.results[n].value` ### Matchers - `sinon.match.any` → `expect.anything()` - `sinon.match.string` → `expect.any(String)` - `sinon.match.object` → `expect.any(Object)` / `expect.objectContaining(...)` ### Sandbox lifecycle 7 files in sdk previously used `sandbox = sinon.createSandbox()` + `sandbox.stub(...)` + `sandbox.restore()`. Converted to direct `vi.spyOn(...)` calls with `afterEach(() => vi.restoreAllMocks())`. ### Fake timers (11 sites in http-registry-server) - `sinon.useFakeTimers({ now: date })` → `vi.useFakeTimers({ now: date })` - `clock.tick(ms)` → `vi.advanceTimersByTime(ms)` - `clock.restore()` → `vi.useRealTimers()` - The local `clock` variable is dropped — vitest uses module-level `vi` accessors. ### Restoration - `sinon.restore()` → `vi.restoreAllMocks()` - `stub.restore()` → `stub.mockRestore()` - `stub.resetHistory()` → `stub.mockClear()` - `stub.reset()` → `stub.mockReset()` ### Types - `sinon.SinonStub` → `MockInstance` from `vitest` - `sinon.SinonStub<[Args], Ret>` → `MockInstance<(...args: Args) => Ret>` - `sinon.SinonSandbox` — removed (no vitest equivalent) - `sinon.SinonFakeTimers` — removed (vi is module-level) ## Tricky patterns ### `sinon.createStubInstance(Class)` (http-registry-server, rebalancer, keyfunder) Replaced with hand-rolled partial mocks typed as the class interface: ```ts // Before: const mock = sinon.createStubInstance(Registry); // After: const mock = { getUri: vi.fn(), getChains: vi.fn(), ... } as unknown as IRegistry; ``` For MultiProvider/MultiProtocolProvider (rebalancer) a prototype-walking helper auto-mocks every prototype method with `vi.fn()`. ### `.withArgs(x).returns(y)` (rebalancer, cli) No vitest equivalent. Replaced with a single `mockImplementation` that branches on the argument: ```ts vi.fn(async (id) => routes[id]?.coreConfig) ``` ### Typed factory pattern for mock objects (rebalancer) `createMockActionTracker()` now returns a mapped type `{ [K in keyof IActionTracker]: Mock<IActionTracker[K]> }` so callers can invoke `actionTracker.syncTransfers.mockRejectedValue(err)` without casts. ### `IExternalBridge` mock (rebalancer) `bridge: any` retained in `InventoryRebalancer.test.ts` — the existing pre-migration typing. Downstream annotations use targeted `unknown[]` + narrow `as QuoteParams` rather than `any` for `.mock.calls` access. ### `getChainMetadata` stub (rebalancer RebalancerContextFactory) SUT reads only `.protocol` off the return. Rather than fabricating a full `ChainMetadata`, cast the partial return `as unknown as ChainMetadata` to make the mock's intent explicit. ## cli setup file fix Previous PR deleted `vitest.setup.ts` but the cli's `vitest.config.ts` still referenced it. Dropped the `../../vitest.setup.ts` entry from cli's setupFiles — only `./src/tests/test-setup.ts` remains. ## devDeps dropped (pure-vitest packages) ccip-server, deploy-sdk, http-registry-server, keyfunder, metrics, provider-sdk (no sinon usage), utils, warp-monitor. Hybrid packages retain `sinon` + `@types/sinon` for hardhat/e2e: sdk, rebalancer, cli, infra, svm-sdk, tron-sdk, relayer. Catalog: `sinon` and `@types/sinon` entries retained (still used by hybrid packages' mocha-run test paths). ## Tests All 1,654+ migrated vitest tests pass. `pnpm build` green at repo root. `pnpm format:check` + `pnpm lint` clean.
1 parent 962904f commit b58b88d

38 files changed

Lines changed: 1817 additions & 1794 deletions

pnpm-lock.yaml

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

typescript/ccip-server/package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,9 @@
5454
"@types/node": "catalog:",
5555
"@types/pg": "^8.16.0",
5656
"@types/pino-http": "^5.8.4",
57-
"@types/sinon": "catalog:",
5857
"@vercel/ncc": "catalog:",
5958
"nodemon": "^3.0.3",
6059
"pino-pretty": "catalog:",
61-
"sinon": "catalog:",
6260
"tsx": "catalog:",
6361
"typescript": "catalog:",
6462
"vite": "catalog:",

typescript/ccip-server/tests/services/CallCommitmentsService.test.ts

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import sinon from 'sinon';
2-
import { describe, expect, it } from 'vitest';
1+
import { describe, expect, it, vi } from 'vitest';
32

43
import {
54
PostCallsSchema,
@@ -11,20 +10,22 @@ import {
1110
import { CallCommitmentsService } from '../../src/services/CallCommitmentsService.js';
1211

1312
function mockLogger() {
14-
return {
15-
info: sinon.stub(),
16-
warn: sinon.stub(),
17-
error: sinon.stub(),
18-
debug: sinon.stub(),
19-
setBindings: sinon.stub(),
20-
child: sinon.stub().returnsThis(),
13+
const logger: Record<string, ReturnType<typeof vi.fn>> = {
14+
info: vi.fn(),
15+
warn: vi.fn(),
16+
error: vi.fn(),
17+
debug: vi.fn(),
18+
setBindings: vi.fn(),
19+
child: vi.fn(),
2120
};
21+
logger.child.mockReturnValue(logger);
22+
return logger;
2223
}
2324

2425
function mockRes() {
25-
const json = sinon.stub();
26-
const status = sinon.stub().returns({ json });
27-
const sendStatus = sinon.stub();
26+
const json = vi.fn();
27+
const status = vi.fn().mockReturnValue({ json });
28+
const sendStatus = vi.fn();
2829
return { status, json, sendStatus };
2930
}
3031

@@ -113,36 +114,36 @@ describe('CallCommitmentsService.handleCommitment', () => {
113114

114115
await service.handleCommitment(req, res);
115116

116-
expect(res.status.calledWith(400)).toBe(true);
117-
expect(res.json.called).toBe(true);
117+
expect(res.status).toHaveBeenCalledWith(400);
118+
expect(res.json).toHaveBeenCalled();
118119
});
119120

120121
it('routes ICA payload to deriveIcaFromConfig', async () => {
121122
const mockIca = '0x' + 'ff'.repeat(20);
122123
const icaApp = {
123-
getAccount: sinon.stub().resolves(mockIca),
124+
getAccount: vi.fn().mockResolvedValue(mockIca),
124125
};
125126
const multiProvider = {
126-
getChainName: sinon.stub().returns('ethereum'),
127-
getProvider: sinon.stub(),
127+
getChainName: vi.fn().mockReturnValue('ethereum'),
128+
getProvider: vi.fn(),
128129
};
129130
const service = createService({ icaApp, multiProvider });
130-
service.upsertCommitmentInDB = sinon.stub().resolves();
131+
service.upsertCommitmentInDB = vi.fn().mockResolvedValue(undefined);
131132

132133
const req = { body: icaPayload, log: mockLogger() };
133134
const res = mockRes();
134135

135136
await service.handleCommitment(req, res);
136137

137-
expect(icaApp.getAccount.called).toBe(true);
138-
expect(res.sendStatus.calledWith(200)).toBe(true);
138+
expect(icaApp.getAccount).toHaveBeenCalled();
139+
expect(res.sendStatus).toHaveBeenCalledWith(200);
139140
});
140141

141142
it('routes legacy payload to deriveIcaFromDispatchTx', async () => {
142143
const multiProvider = {
143-
getChainName: sinon.stub().returns('ethereum'),
144-
getProvider: sinon.stub().returns({
145-
getTransactionReceipt: sinon.stub().resolves(null),
144+
getChainName: vi.fn().mockReturnValue('ethereum'),
145+
getProvider: vi.fn().mockReturnValue({
146+
getTransactionReceipt: vi.fn().mockResolvedValue(null),
146147
}),
147148
};
148149
const service = createService({ multiProvider });
@@ -153,10 +154,10 @@ describe('CallCommitmentsService.handleCommitment', () => {
153154
await service.handleCommitment(req, res);
154155

155156
// Should fail because receipt is null, returning 400
156-
expect(res.status.calledWith(400)).toBe(true);
157-
expect(
158-
multiProvider.getProvider.calledWith(legacyPayload.originDomain),
159-
).toBe(true);
157+
expect(res.status).toHaveBeenCalledWith(400);
158+
expect(multiProvider.getProvider).toHaveBeenCalledWith(
159+
legacyPayload.originDomain,
160+
);
160161
});
161162
});
162163

typescript/cli/src/deploy/warp.test.ts

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { expect } from 'vitest';
2-
import sinon from 'sinon';
1+
import { expect, vi } from 'vitest';
32

43
import {
54
ProtocolType,
@@ -48,16 +47,15 @@ function buildCrossCollateralToken({
4847
function buildContext(
4948
routes: Record<string, { coreConfig: WarpCoreConfig; deployConfig: any }>,
5049
) {
51-
const getWarpRoute = sinon.stub();
52-
const getWarpDeployConfig = sinon.stub();
50+
const getWarpRoute = vi.fn(
51+
async (routeId: string) => routes[routeId]?.coreConfig,
52+
);
53+
const getWarpDeployConfig = vi.fn(
54+
async (routeId: string) => routes[routeId]?.deployConfig,
55+
);
5356

54-
for (const [id, route] of Object.entries(routes)) {
55-
getWarpRoute.withArgs(id).resolves(route.coreConfig);
56-
getWarpDeployConfig.withArgs(id).resolves(route.deployConfig);
57-
}
58-
59-
const addWarpRouteConfig = sinon.stub().resolves();
60-
const addWarpRoute = sinon.stub().resolves();
57+
const addWarpRouteConfig = vi.fn().mockResolvedValue(undefined);
58+
const addWarpRoute = vi.fn().mockResolvedValue(undefined);
6159

6260
return {
6361
context: {
@@ -87,7 +85,7 @@ describe('runWarpRouteCombine', () => {
8785
const ROUTER_C = '0x3333333333333333333333333333333333333333';
8886

8987
afterEach(() => {
90-
sinon.restore();
88+
vi.restoreAllMocks();
9189
});
9290

9391
it('warns when combine will remove previously enrolled routers', async () => {
@@ -137,16 +135,16 @@ describe('runWarpRouteCombine', () => {
137135
'route-a': routeA,
138136
'route-b': routeB,
139137
});
140-
const warnSpy = sinon.spy(rootLogger, 'warn');
138+
const warnSpy = vi.spyOn(rootLogger, 'warn');
141139

142140
await runWarpRouteCombine({
143141
context,
144142
routeIds: ['route-a', 'route-b'],
145143
outputWarpRouteId: 'MULTI/test',
146144
});
147145

148-
expect(warnSpy.called).toBe(true);
149-
const warnings = warnSpy.getCalls().map((call) => String(call.args[0]));
146+
expect(warnSpy).toHaveBeenCalled();
147+
const warnings = warnSpy.mock.calls.map((args) => String(args[0]));
150148
expect(
151149
warnings.some(
152150
(warning) =>
@@ -155,7 +153,7 @@ describe('runWarpRouteCombine', () => {
155153
),
156154
).toBe(true);
157155

158-
const updatedRouteAConfig = addWarpRouteConfig.getCall(0).args[0];
156+
const updatedRouteAConfig = addWarpRouteConfig.mock.calls[0][0];
159157
expect(updatedRouteAConfig.anvil2.crossCollateralRouters).toEqual({
160158
[DOMAIN_BY_CHAIN.anvil3.toString()]: [addressToBytes32(ROUTER_B)],
161159
});

typescript/cli/src/tests/context/create-altvm-signers.test.ts

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { expect } from 'vitest';
2-
import sinon from 'sinon';
1+
import { expect, vi } from 'vitest';
32

43
import {
54
type AltVM,
@@ -60,7 +59,7 @@ describe('createAltVMSigners', () => {
6059
beforeEach(() => {
6160
capturedConfigs.length = 0;
6261
delete process.env.HYP_ACCOUNT_ADDRESS_STARKNET;
63-
sinon.restore();
62+
vi.restoreAllMocks();
6463
});
6564

6665
function getProtocolRegistry(): NonNullable<
@@ -213,9 +212,9 @@ describe('createAltVMSigners', () => {
213212
});
214213

215214
it('prompts for private key when strategy omits it', async () => {
216-
const privateKeyPrompt = sinon
217-
.stub(altVmPrompts, 'password')
218-
.resolves('0xprompted-key');
215+
const privateKeyPrompt = vi
216+
.spyOn(altVmPrompts, 'password')
217+
.mockResolvedValue('0xprompted-key');
219218

220219
const strategy: Partial<ExtendedChainSubmissionStrategy> = {
221220
starknetsepolia: {
@@ -235,7 +234,7 @@ describe('createAltVMSigners', () => {
235234
getProtocolRegistry(),
236235
);
237236

238-
expect(privateKeyPrompt.calledOnce).toBe(true);
237+
expect(privateKeyPrompt).toHaveBeenCalledOnce();
239238
expect(capturedConfigs).toHaveLength(1);
240239
expect(capturedConfigs[0]).toEqual({
241240
privateKey: '0xprompted-key',
@@ -244,7 +243,7 @@ describe('createAltVMSigners', () => {
244243
});
245244

246245
it('throws for non-Starknet jsonRpc strategies without a private key', async () => {
247-
const privateKeyPrompt = sinon.stub(altVmPrompts, 'password');
246+
const privateKeyPrompt = vi.spyOn(altVmPrompts, 'password');
248247

249248
const strategy: Partial<ExtendedChainSubmissionStrategy> = {
250249
radix: {
@@ -272,12 +271,12 @@ describe('createAltVMSigners', () => {
272271
);
273272
});
274273

275-
expect(privateKeyPrompt.called).toBe(false);
274+
expect(privateKeyPrompt).not.toHaveBeenCalled();
276275
expect(capturedConfigs).toHaveLength(0);
277276
});
278277

279278
it('throws for non-jsonRpc strategies instead of prompting for a private key', async () => {
280-
const privateKeyPrompt = sinon.stub(altVmPrompts, 'password');
279+
const privateKeyPrompt = vi.spyOn(altVmPrompts, 'password');
281280

282281
const strategy: Partial<ExtendedChainSubmissionStrategy> = {
283282
radix: {
@@ -306,14 +305,14 @@ describe('createAltVMSigners', () => {
306305
);
307306
});
308307

309-
expect(privateKeyPrompt.called).toBe(false);
308+
expect(privateKeyPrompt).not.toHaveBeenCalled();
310309
expect(capturedConfigs).toHaveLength(0);
311310
});
312311

313312
it('prefers explicit per-chain strategy key over prompted fallback', async () => {
314-
const privateKeyPrompt = sinon
315-
.stub(altVmPrompts, 'password')
316-
.resolves('0xprompted-key');
313+
const privateKeyPrompt = vi
314+
.spyOn(altVmPrompts, 'password')
315+
.mockResolvedValue('0xprompted-key');
317316

318317
const strategy: Partial<ExtendedChainSubmissionStrategy> = {
319318
starknetsepolia: {
@@ -341,7 +340,7 @@ describe('createAltVMSigners', () => {
341340
getProtocolRegistry(),
342341
);
343342

344-
expect(privateKeyPrompt.calledOnce).toBe(true);
343+
expect(privateKeyPrompt).toHaveBeenCalledOnce();
345344
expect(capturedConfigs).toHaveLength(2);
346345
expect(capturedConfigs[0]).toEqual({
347346
privateKey: '0xprompted-key',
@@ -354,12 +353,10 @@ describe('createAltVMSigners', () => {
354353
});
355354

356355
it('does not reuse a prompted Starknet account address across chains', async () => {
357-
const accountPrompt = sinon
358-
.stub(altVmPrompts, 'input')
359-
.onFirstCall()
360-
.resolves('0xaaa')
361-
.onSecondCall()
362-
.resolves('0xbbb');
356+
const accountPrompt = vi
357+
.spyOn(altVmPrompts, 'input')
358+
.mockResolvedValueOnce('0xaaa')
359+
.mockResolvedValueOnce('0xbbb');
363360

364361
const keys: SignerKeyProtocolMap = {
365362
[ProtocolType.Starknet]: '0xkey',
@@ -373,7 +370,7 @@ describe('createAltVMSigners', () => {
373370
getProtocolRegistry(),
374371
);
375372

376-
expect(accountPrompt.callCount).toBe(2);
373+
expect(accountPrompt).toHaveBeenCalledTimes(2);
377374
expect(capturedConfigs).toHaveLength(2);
378375
expect(capturedConfigs[0].accountAddress).toBe('0xaaa');
379376
expect(capturedConfigs[1].accountAddress).toBe('0xbbb');

typescript/keyfunder/package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,8 @@
5151
"devDependencies": {
5252
"@hyperlane-xyz/tsconfig": "workspace:^",
5353
"@types/node": "catalog:",
54-
"@types/sinon": "catalog:",
5554
"@vercel/ncc": "catalog:",
5655
"oxfmt": "catalog:",
57-
"sinon": "catalog:",
5856
"tsx": "catalog:",
5957
"typescript": "catalog:",
6058
"vite": "catalog:",

0 commit comments

Comments
 (0)