Adding some simple tests. (#54)

This commit is contained in:
Evan Senter 2025-04-19 18:07:24 +01:00 committed by GitHub
parent d9ad2a74ae
commit 0c9e1ef61b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 1626 additions and 9 deletions

1433
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -14,6 +14,7 @@
"lint": "eslint . --ext .ts,.tsx",
"typecheck": "tsc --noEmit --jsx react",
"format": "prettier --write .",
"preflight": "npm run format --workspaces --if-present && npm run lint --workspaces --if-present && npm run test --workspaces --if-present",
"artifactregistry-login": "npx google-artifactregistry-auth"
},
"devDependencies": {

View File

@ -10,14 +10,15 @@
"start": "node dist/gemini.js",
"debug": "node --inspect-brk dist/gemini.js",
"lint": "eslint . --ext .ts,.tsx",
"format": "prettier --write ."
"format": "prettier --write .",
"test": "vitest run"
},
"files": [
"dist"
],
"dependencies": {
"@google/genai": "^0.8.0",
"@gemini-code/server": "1.0.0",
"@google/genai": "^0.8.0",
"diff": "^7.0.0",
"dotenv": "^16.4.7",
"fast-glob": "^3.3.3",
@ -34,7 +35,8 @@
"@types/node": "^20.11.24",
"@types/react": "^19.1.0",
"@types/yargs": "^17.0.32",
"typescript": "^5.3.3"
"typescript": "^5.3.3",
"vitest": "^3.1.1"
},
"engines": {
"node": ">=18"

View File

@ -0,0 +1,165 @@
import { describe, it, expect, vi, beforeEach, Mock } from 'vitest';
import { GoogleGenAI, Type, Content } from '@google/genai';
import { GeminiClient } from './gemini-client.js';
import { Config } from '../config/config.js';
// Mock the entire @google/genai module
vi.mock('@google/genai');
// Mock the Config class and its methods
vi.mock('../config/config.js', () => {
// The mock constructor should accept the arguments but not explicitly return an object.
// vi.fn() will create a mock instance that inherits from the prototype.
const MockConfig = vi.fn();
// Methods are mocked on the prototype, so instances will inherit them.
MockConfig.prototype.getApiKey = vi.fn(() => 'mock-api-key');
MockConfig.prototype.getModel = vi.fn(() => 'mock-model');
MockConfig.prototype.getTargetDir = vi.fn(() => 'mock-target-dir');
return { Config: MockConfig };
});
// Define a type for the mocked GoogleGenAI instance structure
type MockGoogleGenAIType = {
models: {
generateContent: Mock;
};
chats: {
create: Mock;
};
};
describe('GeminiClient', () => {
// Use the specific types defined above
let mockGenerateContent: MockGoogleGenAIType['models']['generateContent'];
let mockGoogleGenAIInstance: MockGoogleGenAIType;
let config: Config;
let client: GeminiClient;
beforeEach(() => {
vi.clearAllMocks();
// Mock the generateContent method specifically
mockGenerateContent = vi.fn();
// Mock the chainable structure ai.models.generateContent
mockGoogleGenAIInstance = {
models: {
generateContent: mockGenerateContent,
},
chats: {
create: vi.fn(), // Mock create as well
},
};
// Configure the mocked GoogleGenAI constructor to return our mock instance
(GoogleGenAI as Mock).mockImplementation(() => mockGoogleGenAIInstance);
config = new Config('mock-api-key-arg', 'mock-model-arg', 'mock-dir-arg');
client = new GeminiClient(config);
});
describe('generateJson', () => {
it('should call ai.models.generateContent with correct parameters', async () => {
const mockContents: Content[] = [
{ role: 'user', parts: [{ text: 'test prompt' }] },
];
const mockSchema = {
type: Type.OBJECT,
properties: { key: { type: Type.STRING } },
};
const mockApiResponse = { text: JSON.stringify({ key: 'value' }) };
mockGenerateContent.mockResolvedValue(mockApiResponse);
await client.generateJson(mockContents, mockSchema);
expect(mockGenerateContent).toHaveBeenCalledTimes(1);
// Use expect.objectContaining for the config assertion
const expectedConfigMatcher = expect.objectContaining({
temperature: 0,
topP: 1,
systemInstruction: expect.any(String),
responseSchema: mockSchema,
responseMimeType: 'application/json',
});
expect(mockGenerateContent).toHaveBeenCalledWith({
model: 'mock-model',
config: expectedConfigMatcher,
contents: mockContents,
});
});
it('should return the parsed JSON response', async () => {
const mockContents: Content[] = [
{ role: 'user', parts: [{ text: 'test prompt' }] },
];
const mockSchema = {
type: Type.OBJECT,
properties: { key: { type: Type.STRING } },
};
const expectedJson = { key: 'value' };
const mockApiResponse = { text: JSON.stringify(expectedJson) };
mockGenerateContent.mockResolvedValue(mockApiResponse);
const result = await client.generateJson(mockContents, mockSchema);
expect(result).toEqual(expectedJson);
});
it('should throw an error if API returns empty response', async () => {
const mockContents: Content[] = [
{ role: 'user', parts: [{ text: 'test prompt' }] },
];
const mockSchema = {
type: Type.OBJECT,
properties: { key: { type: Type.STRING } },
};
const mockApiResponse = { text: '' }; // Empty response
mockGenerateContent.mockResolvedValue(mockApiResponse);
await expect(
client.generateJson(mockContents, mockSchema),
).rejects.toThrow(
'Failed to generate JSON content: API returned an empty response.',
);
});
it('should throw an error if API response is not valid JSON', async () => {
const mockContents: Content[] = [
{ role: 'user', parts: [{ text: 'test prompt' }] },
];
const mockSchema = {
type: Type.OBJECT,
properties: { key: { type: Type.STRING } },
};
const mockApiResponse = { text: 'invalid json' }; // Invalid JSON
mockGenerateContent.mockResolvedValue(mockApiResponse);
await expect(
client.generateJson(mockContents, mockSchema),
).rejects.toThrow('Failed to parse API response as JSON:');
});
it('should throw an error if generateContent rejects', async () => {
const mockContents: Content[] = [
{ role: 'user', parts: [{ text: 'test prompt' }] },
];
const mockSchema = {
type: Type.OBJECT,
properties: { key: { type: Type.STRING } },
};
const apiError = new Error('API call failed');
mockGenerateContent.mockRejectedValue(apiError);
await expect(
client.generateJson(mockContents, mockSchema),
).rejects.toThrow(`Failed to generate JSON content: ${apiError.message}`);
});
});
// TODO: Add tests for startChat and sendMessageStream later
});

View File

@ -0,0 +1,9 @@
import { describe, it, expect } from 'vitest';
import { toolRegistry } from './tools/tool-registry.js';
describe('cli tests', () => {
it('should have a tool registry', () => {
expect(toolRegistry).toBeDefined();
expect(typeof toolRegistry.registerTool).toBe('function');
});
});

View File

@ -9,7 +9,8 @@
"target": "ES2020",
"paths": {
"@gemini-code/*": ["./packages/*"]
}
},
"types": ["node", "vitest/globals"]
},
"exclude": ["node_modules", "dist"],
"include": ["src"],

View File

@ -8,14 +8,15 @@
"build": "tsc --build && cp package.json dist/",
"clean": "rm -rf dist",
"lint": "eslint . --ext .ts,.tsx",
"format": "prettier --write ."
"format": "prettier --write .",
"test": "vitest run"
},
"files": [
"dist"
],
"dependencies": {},
"devDependencies": {
"typescript": "^5.3.3"
"typescript": "^5.3.3",
"vitest": "^3.1.1"
},
"engines": {
"node": ">=18"

View File

@ -0,0 +1,9 @@
import { describe, it, expect } from 'vitest';
import { helloServer } from './index.js';
describe('server tests', () => {
it('should export helloServer function', () => {
expect(helloServer).toBeDefined();
expect(typeof helloServer).toBe('function');
});
});