test(ai): migrate tests to Vitest - #10363
Conversation
|
Vertex AI Mock Responses Check
|
There was a problem hiding this comment.
Code Review
This pull request migrates the test suite for the packages/ai package from Karma and Mocha to Vitest, which includes updating package scripts, adding Vitest configuration files, and refactoring several test files to use Vitest's mocking utilities (vi.mock, vi.hoisted). Feedback on these changes highlights a regression where TypeScript type checking was inadvertently disabled in both local and CI test scripts. Additionally, in chat-session.test.ts and template-chat-session.test.ts, the custom stubbing mechanism lacks a corresponding custom restore function to reset the mocked properties, which could lead to test pollution and flaky tests.
| "test": "run-p --npm-path npm lint test:all", | ||
| "test:ci": "node ../../scripts/run_tests_in_ci.js -s test:all", |
There was a problem hiding this comment.
Removing type-check from the test script and changing test:ci to run test:all instead of test completely disables TypeScript type checking in both local testing and CI. This is a regression that could allow type errors to be merged. Please restore type-check to the test script and configure test:ci to run test.
| "test": "run-p --npm-path npm lint test:all", | |
| "test:ci": "node ../../scripts/run_tests_in_ci.js -s test:all", | |
| "test": "run-p --npm-path npm lint type-check test:all", | |
| "test:ci": "node ../../scripts/run_tests_in_ci.js -s test", |
| import { | ||
| match, | ||
| restore, | ||
| SinonSpy, | ||
| spy, | ||
| stub as sinonStub, | ||
| useFakeTimers | ||
| } from 'sinon'; | ||
| import sinonChai from 'sinon-chai'; | ||
| import chaiAsPromised from 'chai-as-promised'; | ||
| import * as generateContentMethods from './generate-content'; | ||
|
|
||
| const { mockGenerateContent } = vi.hoisted(() => ({ | ||
| mockGenerateContent: { | ||
| generateContent: (..._args: any[]): any => {}, | ||
| generateContentStream: (..._args: any[]): any => {} | ||
| } | ||
| })); | ||
|
|
||
| vi.mock('./generate-content', async importOriginal => { | ||
| const actual = await importOriginal<any>(); | ||
| return { | ||
| ...actual, | ||
| generateContent: (...args: any[]) => | ||
| mockGenerateContent.generateContent(...args), | ||
| generateContentStream: (...args: any[]) => | ||
| mockGenerateContent.generateContentStream(...args) | ||
| }; | ||
| }); | ||
|
|
||
| function stub(obj?: any, method?: any): any { | ||
| if (obj === generateContentMethods) { | ||
| const s = sinonStub(); | ||
| (mockGenerateContent as any)[method] = s; | ||
| return s; | ||
| } | ||
| return (sinonStub as any)(...arguments); | ||
| } |
There was a problem hiding this comment.
The custom stub function intercepts calls to generateContentMethods and assigns anonymous stubs to mockGenerateContent. However, since these are anonymous stubs and not registered with sinon's global sandbox, calling restore() (which is imported directly from sinon) will not reset these stubs between tests. This leads to test pollution and flaky tests. Define a custom restore function that calls sinonRestore() and resets the mockGenerateContent properties back to their default dummy functions.
import {
match,
restore as sinonRestore,
SinonSpy,
spy,
stub as sinonStub,
useFakeTimers
} from 'sinon';
import sinonChai from 'sinon-chai';
import chaiAsPromised from 'chai-as-promised';
import * as generateContentMethods from './generate-content';
const { mockGenerateContent } = vi.hoisted(() => ({
mockGenerateContent: {
generateContent: (..._args: any[]): any => {},
generateContentStream: (..._args: any[]): any => {}
}
}));
vi.mock('./generate-content', async importOriginal => {
const actual = await importOriginal<any>();
return {
...actual,
generateContent: (...args: any[]) =>
mockGenerateContent.generateContent(...args),
generateContentStream: (...args: any[]) =>
mockGenerateContent.generateContentStream(...args)
};
});
function restore(): void {
sinonRestore();
mockGenerateContent.generateContent = (..._args: any[]): any => {};
mockGenerateContent.generateContentStream = (..._args: any[]): any => {};
}
function stub(obj?: any, method?: any): any {
if (obj === generateContentMethods) {
const s = sinonStub();
(mockGenerateContent as any)[method] = s;
return s;
}
return (sinonStub as any)(...arguments);
}| import { match, restore, SinonSpy, spy, stub as sinonStub } from 'sinon'; | ||
| import sinonChai from 'sinon-chai'; | ||
| import chaiAsPromised from 'chai-as-promised'; | ||
| import * as generateContentMethods from './generate-content'; | ||
|
|
||
| const { mockGenerateContent } = vi.hoisted(() => ({ | ||
| mockGenerateContent: { | ||
| templateGenerateContent: (..._args: any[]): any => {}, | ||
| templateGenerateContentStream: (..._args: any[]): any => {} | ||
| } | ||
| })); | ||
|
|
||
| vi.mock('./generate-content', async importOriginal => { | ||
| const actual = await importOriginal<any>(); | ||
| return { | ||
| ...actual, | ||
| templateGenerateContent: (...args: any[]) => | ||
| mockGenerateContent.templateGenerateContent(...args), | ||
| templateGenerateContentStream: (...args: any[]) => | ||
| mockGenerateContent.templateGenerateContentStream(...args) | ||
| }; | ||
| }); | ||
|
|
||
| function stub(obj?: any, method?: any): any { | ||
| if (obj === generateContentMethods) { | ||
| const s = sinonStub(); | ||
| (mockGenerateContent as any)[method] = s; | ||
| return s; | ||
| } | ||
| return (sinonStub as any)(...arguments); | ||
| } |
There was a problem hiding this comment.
The custom stub function intercepts calls to generateContentMethods and assigns anonymous stubs to mockGenerateContent. However, since these are anonymous stubs and not registered with sinon's global sandbox, calling restore() (which is imported directly from sinon) will not reset these stubs between tests. This leads to test pollution and flaky tests. Define a custom restore function that calls sinonRestore() and resets the mockGenerateContent properties back to their default dummy functions.
import { match, restore as sinonRestore, SinonSpy, spy, stub as sinonStub } from 'sinon';
import sinonChai from 'sinon-chai';
import chaiAsPromised from 'chai-as-promised';
import * as generateContentMethods from './generate-content';
const { mockGenerateContent } = vi.hoisted(() => ({
mockGenerateContent: {
templateGenerateContent: (..._args: any[]): any => {},
templateGenerateContentStream: (..._args: any[]): any => {}
}
}));
vi.mock('./generate-content', async importOriginal => {
const actual = await importOriginal<any>();
return {
...actual,
templateGenerateContent: (...args: any[]) =>
mockGenerateContent.templateGenerateContent(...args),
templateGenerateContentStream: (...args: any[]) =>
mockGenerateContent.templateGenerateContentStream(...args)
};
});
function restore(): void {
sinonRestore();
mockGenerateContent.templateGenerateContent = (..._args: any[]): any => {};
mockGenerateContent.templateGenerateContentStream = (..._args: any[]): any => {};
}
function stub(obj?: any, method?: any): any {
if (obj === generateContentMethods) {
const s = sinonStub();
(mockGenerateContent as any)[method] = s;
return s;
}
return (sinonStub as any)(...arguments);
}a5ab877 to
52d7592
Compare
- Replace anonymous stubs and manual afterEach null-resets with delegated mock implementations in count-tokens and generate-content tests. - Remove custom restore override in generative-model tests in favor of standard sinon.restore. - Use native Vitest runIf conditional runners in chrome-adapter and live-session-helpers tests to eliminate ESLint bypass comments. TAG=agy CONV=0e4d0bde-5c64-450d-ab5a-2a4f7489262c
| controller.abort(abortReason); | ||
|
|
||
| await expect(requestPromise).to.be.rejectedWith( | ||
| const assertion = expect(requestPromise).to.be.rejectedWith( |
There was a problem hiding this comment.
Attaching the rejection listener before ticking the clock. Otherwise, ticking the clock triggers the timeout and rejects the promise before the handler is attached, causing an UnhandledPromiseRejection error in Vitest.
| mockWebSocket.triggerMessage(new Blob([JSON.stringify({ foo: 2 })])); | ||
|
|
||
| await clock.tickAsync(5); | ||
| while (received.length < 2) { |
There was a problem hiding this comment.
[test improvement- not related to migration] Blob.text() is an async promise not controlled by fake timers. Wait for both messages to finish decoding before closing the socket.
| let webSocketStub: SinonStub; | ||
|
|
||
| beforeEach(() => { | ||
| if (typeof (globalThis as any).WebSocket === 'undefined') { |
There was a problem hiding this comment.
[test improvement- node tests crashing on node 20] Node 20 lacks global WebSocket (added in Node 22). This dummy class prevents sinon.stub(globalThis, 'WebSocket') from crashing on Node 20.
| import sinonChai from 'sinon-chai'; | ||
| import chaiAsPromised from 'chai-as-promised'; | ||
| import * as generateContentMethods from './generate-content'; | ||
|
|
There was a problem hiding this comment.
ESM modules are frozen in Vitest, so sinon.stub() can't modify them directly. This proxy delegates to real code by default, letting us keep existing Sinon stubs.
…n-Chai patterns - Replace legacy Chai assertions with native Vitest matchers - Eliminate Sinon stub and spy helpers in favor of native vi.spyOn and vi.fn implementations - Replace custom match and match.any helpers with expect.anything(), expect.objectContaining(), and expect.toSatisfy() - Establish test/setup.ts to restore mocks and reset timers across test runs
Description
Migrates
@firebase/aiunit tests from legacy Karma & Mocha to Vitest (Node + Browser Chromium).Key Changes
vitest.config.mjsextendingconfig/vitest.base.mjsand removed deprecatedkarma.conf.js.npm testscripts to purevitestcommands (test:all,test:node,test:browser).globalreferences with standard ECMAScriptglobalThisinlive-session-helpers.test.ts.this.skip()anddescribeearly returns with conditional runner skips (describe.skip/it.skip) inchrome-adapter.test.ts.clock.tickAsyncinrequest.test.ts.websocket.test.ts.src/api.tsandsrc/types/index.ts.test/types/vitest-globals.d.tsfor isolated test typings.Testing & Impact