Add test coverage

This commit is contained in:
Matt Rubens
2025-01-07 01:52:42 -05:00
parent 537514de44
commit 5fbfe9b775
19 changed files with 3106 additions and 493 deletions

View File

@@ -0,0 +1,97 @@
import { calculateApiCost } from '../cost';
import { ModelInfo } from '../../shared/api';
describe('Cost Utility', () => {
describe('calculateApiCost', () => {
const mockModelInfo: ModelInfo = {
maxTokens: 8192,
contextWindow: 200_000,
supportsPromptCache: true,
inputPrice: 3.0, // $3 per million tokens
outputPrice: 15.0, // $15 per million tokens
cacheWritesPrice: 3.75, // $3.75 per million tokens
cacheReadsPrice: 0.3, // $0.30 per million tokens
};
it('should calculate basic input/output costs correctly', () => {
const cost = calculateApiCost(mockModelInfo, 1000, 500);
// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
// Total: 0.003 + 0.0075 = 0.0105
expect(cost).toBe(0.0105);
});
it('should handle cache writes cost', () => {
const cost = calculateApiCost(mockModelInfo, 1000, 500, 2000);
// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
// Cache writes: (3.75 / 1_000_000) * 2000 = 0.0075
// Total: 0.003 + 0.0075 + 0.0075 = 0.018
expect(cost).toBeCloseTo(0.018, 6);
});
it('should handle cache reads cost', () => {
const cost = calculateApiCost(mockModelInfo, 1000, 500, undefined, 3000);
// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
// Cache reads: (0.3 / 1_000_000) * 3000 = 0.0009
// Total: 0.003 + 0.0075 + 0.0009 = 0.0114
expect(cost).toBe(0.0114);
});
it('should handle all cost components together', () => {
const cost = calculateApiCost(mockModelInfo, 1000, 500, 2000, 3000);
// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
// Cache writes: (3.75 / 1_000_000) * 2000 = 0.0075
// Cache reads: (0.3 / 1_000_000) * 3000 = 0.0009
// Total: 0.003 + 0.0075 + 0.0075 + 0.0009 = 0.0189
expect(cost).toBe(0.0189);
});
it('should handle missing prices gracefully', () => {
const modelWithoutPrices: ModelInfo = {
maxTokens: 8192,
contextWindow: 200_000,
supportsPromptCache: true
};
const cost = calculateApiCost(modelWithoutPrices, 1000, 500, 2000, 3000);
expect(cost).toBe(0);
});
it('should handle zero tokens', () => {
const cost = calculateApiCost(mockModelInfo, 0, 0, 0, 0);
expect(cost).toBe(0);
});
it('should handle undefined cache values', () => {
const cost = calculateApiCost(mockModelInfo, 1000, 500);
// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
// Total: 0.003 + 0.0075 = 0.0105
expect(cost).toBe(0.0105);
});
it('should handle missing cache prices', () => {
const modelWithoutCachePrices: ModelInfo = {
...mockModelInfo,
cacheWritesPrice: undefined,
cacheReadsPrice: undefined
};
const cost = calculateApiCost(modelWithoutCachePrices, 1000, 500, 2000, 3000);
// Should only include input and output costs
// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
// Total: 0.003 + 0.0075 = 0.0105
expect(cost).toBe(0.0105);
});
});
});

View File

@@ -0,0 +1,336 @@
import { jest } from '@jest/globals'
import { searchCommits, getCommitInfo, getWorkingState, GitCommit } from '../git'
import { ExecException } from 'child_process'
type ExecFunction = (
command: string,
options: { cwd?: string },
callback: (error: ExecException | null, result?: { stdout: string; stderr: string }) => void
) => void
type PromisifiedExec = (command: string, options?: { cwd?: string }) => Promise<{ stdout: string; stderr: string }>
// Mock child_process.exec
jest.mock('child_process', () => ({
exec: jest.fn()
}))
// Mock util.promisify to return our own mock function
jest.mock('util', () => ({
promisify: jest.fn((fn: ExecFunction): PromisifiedExec => {
return async (command: string, options?: { cwd?: string }) => {
// Call the original mock to maintain the mock implementation
return new Promise((resolve, reject) => {
fn(command, options || {}, (error: ExecException | null, result?: { stdout: string; stderr: string }) => {
if (error) {
reject(error)
} else {
resolve(result!)
}
})
})
}
})
}))
// Mock extract-text
jest.mock('../../integrations/misc/extract-text', () => ({
truncateOutput: jest.fn(text => text)
}))
describe('git utils', () => {
// Get the mock with proper typing
const { exec } = jest.requireMock('child_process') as { exec: jest.MockedFunction<ExecFunction> }
const cwd = '/test/path'
beforeEach(() => {
jest.clearAllMocks()
})
describe('searchCommits', () => {
const mockCommitData = [
'abc123def456',
'abc123',
'fix: test commit',
'John Doe',
'2024-01-06',
'def456abc789',
'def456',
'feat: new feature',
'Jane Smith',
'2024-01-05'
].join('\n')
it('should return commits when git is installed and repo exists', async () => {
// Set up mock responses
const responses = new Map([
['git --version', { stdout: 'git version 2.39.2', stderr: '' }],
['git rev-parse --git-dir', { stdout: '.git', stderr: '' }],
['git log -n 10 --format="%H%n%h%n%s%n%an%n%ad" --date=short --grep="test" --regexp-ignore-case', { stdout: mockCommitData, stderr: '' }]
])
exec.mockImplementation((command: string, options: { cwd?: string }, callback: Function) => {
// Find matching response
for (const [cmd, response] of responses) {
if (command === cmd) {
callback(null, response)
return
}
}
callback(new Error(`Unexpected command: ${command}`))
})
const result = await searchCommits('test', cwd)
// First verify the result is correct
expect(result).toHaveLength(2)
expect(result[0]).toEqual({
hash: 'abc123def456',
shortHash: 'abc123',
subject: 'fix: test commit',
author: 'John Doe',
date: '2024-01-06'
})
// Then verify all commands were called correctly
expect(exec).toHaveBeenCalledWith(
'git --version',
{},
expect.any(Function)
)
expect(exec).toHaveBeenCalledWith(
'git rev-parse --git-dir',
{ cwd },
expect.any(Function)
)
expect(exec).toHaveBeenCalledWith(
'git log -n 10 --format="%H%n%h%n%s%n%an%n%ad" --date=short --grep="test" --regexp-ignore-case',
{ cwd },
expect.any(Function)
)
}, 20000)
it('should return empty array when git is not installed', async () => {
exec.mockImplementation((command: string, options: { cwd?: string }, callback: Function) => {
if (command === 'git --version') {
callback(new Error('git not found'))
return
}
callback(new Error('Unexpected command'))
})
const result = await searchCommits('test', cwd)
expect(result).toEqual([])
expect(exec).toHaveBeenCalledWith('git --version', {}, expect.any(Function))
})
it('should return empty array when not in a git repository', async () => {
const responses = new Map([
['git --version', { stdout: 'git version 2.39.2', stderr: '' }],
['git rev-parse --git-dir', null] // null indicates error should be called
])
exec.mockImplementation((command: string, options: { cwd?: string }, callback: Function) => {
const response = responses.get(command)
if (response === null) {
callback(new Error('not a git repository'))
} else if (response) {
callback(null, response)
} else {
callback(new Error('Unexpected command'))
}
})
const result = await searchCommits('test', cwd)
expect(result).toEqual([])
expect(exec).toHaveBeenCalledWith('git --version', {}, expect.any(Function))
expect(exec).toHaveBeenCalledWith('git rev-parse --git-dir', { cwd }, expect.any(Function))
})
it('should handle hash search when grep search returns no results', async () => {
const responses = new Map([
['git --version', { stdout: 'git version 2.39.2', stderr: '' }],
['git rev-parse --git-dir', { stdout: '.git', stderr: '' }],
['git log -n 10 --format="%H%n%h%n%s%n%an%n%ad" --date=short --grep="abc123" --regexp-ignore-case', { stdout: '', stderr: '' }],
['git log -n 10 --format="%H%n%h%n%s%n%an%n%ad" --date=short --author-date-order abc123', { stdout: mockCommitData, stderr: '' }]
])
exec.mockImplementation((command: string, options: { cwd?: string }, callback: Function) => {
for (const [cmd, response] of responses) {
if (command === cmd) {
callback(null, response)
return
}
}
callback(new Error('Unexpected command'))
})
const result = await searchCommits('abc123', cwd)
expect(result).toHaveLength(2)
expect(result[0]).toEqual({
hash: 'abc123def456',
shortHash: 'abc123',
subject: 'fix: test commit',
author: 'John Doe',
date: '2024-01-06'
})
})
})
describe('getCommitInfo', () => {
const mockCommitInfo = [
'abc123def456',
'abc123',
'fix: test commit',
'John Doe',
'2024-01-06',
'Detailed description'
].join('\n')
const mockStats = '1 file changed, 2 insertions(+), 1 deletion(-)'
const mockDiff = '@@ -1,1 +1,2 @@\n-old line\n+new line'
it('should return formatted commit info', async () => {
const responses = new Map([
['git --version', { stdout: 'git version 2.39.2', stderr: '' }],
['git rev-parse --git-dir', { stdout: '.git', stderr: '' }],
['git show --format="%H%n%h%n%s%n%an%n%ad%n%b" --no-patch abc123', { stdout: mockCommitInfo, stderr: '' }],
['git show --stat --format="" abc123', { stdout: mockStats, stderr: '' }],
['git show --format="" abc123', { stdout: mockDiff, stderr: '' }]
])
exec.mockImplementation((command: string, options: { cwd?: string }, callback: Function) => {
for (const [cmd, response] of responses) {
if (command.startsWith(cmd)) {
callback(null, response)
return
}
}
callback(new Error('Unexpected command'))
})
const result = await getCommitInfo('abc123', cwd)
expect(result).toContain('Commit: abc123')
expect(result).toContain('Author: John Doe')
expect(result).toContain('Files Changed:')
expect(result).toContain('Full Changes:')
})
it('should return error message when git is not installed', async () => {
exec.mockImplementation((command: string, options: { cwd?: string }, callback: Function) => {
if (command === 'git --version') {
callback(new Error('git not found'))
return
}
callback(new Error('Unexpected command'))
})
const result = await getCommitInfo('abc123', cwd)
expect(result).toBe('Git is not installed')
})
it('should return error message when not in a git repository', async () => {
const responses = new Map([
['git --version', { stdout: 'git version 2.39.2', stderr: '' }],
['git rev-parse --git-dir', null] // null indicates error should be called
])
exec.mockImplementation((command: string, options: { cwd?: string }, callback: Function) => {
const response = responses.get(command)
if (response === null) {
callback(new Error('not a git repository'))
} else if (response) {
callback(null, response)
} else {
callback(new Error('Unexpected command'))
}
})
const result = await getCommitInfo('abc123', cwd)
expect(result).toBe('Not a git repository')
})
})
describe('getWorkingState', () => {
const mockStatus = ' M src/file1.ts\n?? src/file2.ts'
const mockDiff = '@@ -1,1 +1,2 @@\n-old line\n+new line'
it('should return working directory changes', async () => {
const responses = new Map([
['git --version', { stdout: 'git version 2.39.2', stderr: '' }],
['git rev-parse --git-dir', { stdout: '.git', stderr: '' }],
['git status --short', { stdout: mockStatus, stderr: '' }],
['git diff HEAD', { stdout: mockDiff, stderr: '' }]
])
exec.mockImplementation((command: string, options: { cwd?: string }, callback: Function) => {
for (const [cmd, response] of responses) {
if (command === cmd) {
callback(null, response)
return
}
}
callback(new Error('Unexpected command'))
})
const result = await getWorkingState(cwd)
expect(result).toContain('Working directory changes:')
expect(result).toContain('src/file1.ts')
expect(result).toContain('src/file2.ts')
})
it('should return message when working directory is clean', async () => {
const responses = new Map([
['git --version', { stdout: 'git version 2.39.2', stderr: '' }],
['git rev-parse --git-dir', { stdout: '.git', stderr: '' }],
['git status --short', { stdout: '', stderr: '' }]
])
exec.mockImplementation((command: string, options: { cwd?: string }, callback: Function) => {
for (const [cmd, response] of responses) {
if (command === cmd) {
callback(null, response)
return
}
}
callback(new Error('Unexpected command'))
})
const result = await getWorkingState(cwd)
expect(result).toBe('No changes in working directory')
})
it('should return error message when git is not installed', async () => {
exec.mockImplementation((command: string, options: { cwd?: string }, callback: Function) => {
if (command === 'git --version') {
callback(new Error('git not found'))
return
}
callback(new Error('Unexpected command'))
})
const result = await getWorkingState(cwd)
expect(result).toBe('Git is not installed')
})
it('should return error message when not in a git repository', async () => {
const responses = new Map([
['git --version', { stdout: 'git version 2.39.2', stderr: '' }],
['git rev-parse --git-dir', null] // null indicates error should be called
])
exec.mockImplementation((command: string, options: { cwd?: string }, callback: Function) => {
const response = responses.get(command)
if (response === null) {
callback(new Error('not a git repository'))
} else if (response) {
callback(null, response)
} else {
callback(new Error('Unexpected command'))
}
})
const result = await getWorkingState(cwd)
expect(result).toBe('Not a git repository')
})
})
})

View File

@@ -0,0 +1,135 @@
import { arePathsEqual, getReadablePath } from '../path';
import * as path from 'path';
import os from 'os';
describe('Path Utilities', () => {
const originalPlatform = process.platform;
afterEach(() => {
Object.defineProperty(process, 'platform', {
value: originalPlatform
});
});
describe('String.prototype.toPosix', () => {
it('should convert backslashes to forward slashes', () => {
const windowsPath = 'C:\\Users\\test\\file.txt';
expect(windowsPath.toPosix()).toBe('C:/Users/test/file.txt');
});
it('should not modify paths with forward slashes', () => {
const unixPath = '/home/user/file.txt';
expect(unixPath.toPosix()).toBe('/home/user/file.txt');
});
it('should preserve extended-length Windows paths', () => {
const extendedPath = '\\\\?\\C:\\Very\\Long\\Path';
expect(extendedPath.toPosix()).toBe('\\\\?\\C:\\Very\\Long\\Path');
});
});
describe('arePathsEqual', () => {
describe('on Windows', () => {
beforeEach(() => {
Object.defineProperty(process, 'platform', {
value: 'win32'
});
});
it('should compare paths case-insensitively', () => {
expect(arePathsEqual('C:\\Users\\Test', 'c:\\users\\test')).toBe(true);
});
it('should handle different path separators', () => {
// Convert both paths to use forward slashes after normalization
const path1 = path.normalize('C:\\Users\\Test').replace(/\\/g, '/');
const path2 = path.normalize('C:/Users/Test').replace(/\\/g, '/');
expect(arePathsEqual(path1, path2)).toBe(true);
});
it('should normalize paths with ../', () => {
// Convert both paths to use forward slashes after normalization
const path1 = path.normalize('C:\\Users\\Test\\..\\Test').replace(/\\/g, '/');
const path2 = path.normalize('C:\\Users\\Test').replace(/\\/g, '/');
expect(arePathsEqual(path1, path2)).toBe(true);
});
});
describe('on POSIX', () => {
beforeEach(() => {
Object.defineProperty(process, 'platform', {
value: 'darwin'
});
});
it('should compare paths case-sensitively', () => {
expect(arePathsEqual('/Users/Test', '/Users/test')).toBe(false);
});
it('should normalize paths', () => {
expect(arePathsEqual('/Users/./Test', '/Users/Test')).toBe(true);
});
it('should handle trailing slashes', () => {
expect(arePathsEqual('/Users/Test/', '/Users/Test')).toBe(true);
});
});
describe('edge cases', () => {
it('should handle undefined paths', () => {
expect(arePathsEqual(undefined, undefined)).toBe(true);
expect(arePathsEqual('/test', undefined)).toBe(false);
expect(arePathsEqual(undefined, '/test')).toBe(false);
});
it('should handle root paths with trailing slashes', () => {
expect(arePathsEqual('/', '/')).toBe(true);
expect(arePathsEqual('C:\\', 'C:\\')).toBe(true);
});
});
});
describe('getReadablePath', () => {
const homeDir = os.homedir();
const desktop = path.join(homeDir, 'Desktop');
it('should return basename when path equals cwd', () => {
const cwd = '/Users/test/project';
expect(getReadablePath(cwd, cwd)).toBe('project');
});
it('should return relative path when inside cwd', () => {
const cwd = '/Users/test/project';
const filePath = '/Users/test/project/src/file.txt';
expect(getReadablePath(cwd, filePath)).toBe('src/file.txt');
});
it('should return absolute path when outside cwd', () => {
const cwd = '/Users/test/project';
const filePath = '/Users/test/other/file.txt';
expect(getReadablePath(cwd, filePath)).toBe('/Users/test/other/file.txt');
});
it('should handle Desktop as cwd', () => {
const filePath = path.join(desktop, 'file.txt');
expect(getReadablePath(desktop, filePath)).toBe(filePath.toPosix());
});
it('should handle undefined relative path', () => {
const cwd = '/Users/test/project';
expect(getReadablePath(cwd)).toBe('project');
});
it('should handle parent directory traversal', () => {
const cwd = '/Users/test/project';
const filePath = '../../other/file.txt';
expect(getReadablePath(cwd, filePath)).toBe('/Users/other/file.txt');
});
it('should normalize paths with redundant segments', () => {
const cwd = '/Users/test/project';
const filePath = '/Users/test/project/./src/../src/file.txt';
expect(getReadablePath(cwd, filePath)).toBe('src/file.txt');
});
});
});