Files
wehub-resource-sync bb5c75ce05
Component Security Validation / Security Audit (push) Has been cancelled
Deploy to Cloudflare Pages / deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:38:58 +08:00

319 lines
10 KiB
JavaScript

/**
* Unit Tests for DataCache
* Tests multi-level caching functionality
*/
const DataCache = require('../../src/analytics/data/DataCache');
const fs = require('fs-extra');
// Mock fs-extra
jest.mock('fs-extra');
describe('DataCache', () => {
let dataCache;
beforeEach(() => {
dataCache = new DataCache();
jest.clearAllMocks();
});
describe('constructor', () => {
it('should initialize with default configuration', () => {
expect(dataCache.caches.fileContent).toBeInstanceOf(Map);
expect(dataCache.caches.parsedData).toBeInstanceOf(Map);
expect(dataCache.caches.computationResults).toBeInstanceOf(Map);
expect(dataCache.metrics.hits).toBe(0);
expect(dataCache.metrics.misses).toBe(0);
});
it('should accept custom options', () => {
const customCache = new DataCache({
fileContentTTL: 10000,
maxFileSize: 2000000
});
expect(customCache.options.fileContentTTL).toBe(10000);
expect(customCache.options.maxFileSize).toBe(2000000);
});
});
describe('getFileContent', () => {
const mockFilePath = '/test/file.jsonl';
const mockContent = '{"test": "data"}';
const mockStats = {
mtime: new Date(Date.now() - 1000),
size: 1024
};
beforeEach(() => {
fs.stat.mockResolvedValue(mockStats);
fs.readFile.mockResolvedValue(mockContent);
});
it('should read file when not cached', async () => {
const content = await dataCache.getFileContent(mockFilePath);
expect(content).toBe(mockContent);
expect(fs.stat).toHaveBeenCalledWith(mockFilePath);
expect(fs.readFile).toHaveBeenCalledWith(mockFilePath, 'utf8');
expect(dataCache.metrics.misses).toBe(1);
expect(dataCache.metrics.hits).toBe(0);
});
it('should return cached content when file unchanged', async () => {
// First call - cache miss
await dataCache.getFileContent(mockFilePath);
// Second call - cache hit
const content = await dataCache.getFileContent(mockFilePath);
expect(content).toBe(mockContent);
expect(fs.readFile).toHaveBeenCalledTimes(1); // Only called once
expect(dataCache.metrics.hits).toBe(1);
expect(dataCache.metrics.misses).toBe(1);
});
it('should re-read file when modified', async () => {
// First call
await dataCache.getFileContent(mockFilePath);
// Simulate file modification
const newerStats = {
mtime: new Date(Date.now() + 1000),
size: 1024
};
fs.stat.mockResolvedValue(newerStats);
fs.readFile.mockResolvedValue('{"updated": "data"}');
// Second call
const content = await dataCache.getFileContent(mockFilePath);
expect(content).toBe('{"updated": "data"}');
expect(fs.readFile).toHaveBeenCalledTimes(2);
expect(dataCache.metrics.misses).toBe(2);
});
it('should handle file read errors', async () => {
fs.stat.mockRejectedValue(new Error('File not found'));
await expect(dataCache.getFileContent(mockFilePath)).rejects.toThrow('File not found');
});
it('should skip large files', async () => {
const largeFileStats = {
mtime: new Date(),
size: 11 * 1024 * 1024 // 11MB - exceeds default 10MB limit
};
fs.stat.mockResolvedValue(largeFileStats);
await expect(dataCache.getFileContent(mockFilePath)).rejects.toThrow('File too large');
});
});
describe('getParsedConversation', () => {
const mockFilePath = '/test/conversation.jsonl';
const mockContent = '{"message": {"role": "user", "content": "hello"}}\n{"message": {"role": "assistant", "content": "hi"}}';
beforeEach(() => {
// Mock getFileContent to return our test data
jest.spyOn(dataCache, 'getFileContent').mockResolvedValue(mockContent);
});
it('should parse conversation content correctly', async () => {
const parsed = await dataCache.getParsedConversation(mockFilePath);
expect(parsed).toHaveLength(2);
expect(parsed[0]).toMatchObject({
role: 'user',
content: 'hello'
});
expect(parsed[1]).toMatchObject({
role: 'assistant',
content: 'hi'
});
});
it('should cache parsed results', async () => {
// First call
await dataCache.getParsedConversation(mockFilePath);
// Second call
await dataCache.getParsedConversation(mockFilePath);
// getFileContent should only be called once due to caching
expect(dataCache.getFileContent).toHaveBeenCalledTimes(1);
});
it('should handle malformed JSON lines', async () => {
const badContent = '{"valid": "json"}\n{invalid json}\n{"another": "valid"}';
dataCache.getFileContent.mockResolvedValue(badContent);
const parsed = await dataCache.getParsedConversation(mockFilePath);
expect(parsed).toHaveLength(2); // Should skip the invalid line
});
it('should handle empty files', async () => {
dataCache.getFileContent.mockResolvedValue('');
const parsed = await dataCache.getParsedConversation(mockFilePath);
expect(parsed).toHaveLength(0);
});
});
describe('getCachedTokenUsage', () => {
const mockFilePath = '/test/file.jsonl';
const mockComputeFunction = jest.fn().mockReturnValue({
total: 1500,
input: 800,
output: 700
});
it('should compute and cache token usage', async () => {
const result = await dataCache.getCachedTokenUsage(mockFilePath, mockComputeFunction);
expect(result).toEqual({
total: 1500,
input: 800,
output: 700
});
expect(mockComputeFunction).toHaveBeenCalledTimes(1);
});
it('should return cached result on subsequent calls', async () => {
// First call
await dataCache.getCachedTokenUsage(mockFilePath, mockComputeFunction);
// Second call
const result = await dataCache.getCachedTokenUsage(mockFilePath, mockComputeFunction);
expect(result).toEqual({
total: 1500,
input: 800,
output: 700
});
expect(mockComputeFunction).toHaveBeenCalledTimes(1); // Only called once
});
it('should handle computation errors', async () => {
const errorFunction = jest.fn().mockImplementation(() => {
throw new Error('Computation failed');
});
await expect(dataCache.getCachedTokenUsage(mockFilePath, errorFunction)).rejects.toThrow('Computation failed');
});
});
describe('invalidateFile', () => {
const mockFilePath = '/test/file.jsonl';
it('should remove all cache entries for a file', async () => {
// Populate caches
dataCache.caches.fileContent.set(mockFilePath, { content: 'test', timestamp: Date.now() });
dataCache.caches.parsedData.set(mockFilePath, { data: 'test', timestamp: Date.now() });
dataCache.caches.computationResults.set(`tokens_${mockFilePath}`, { result: 'test', timestamp: Date.now() });
dataCache.invalidateFile(mockFilePath);
expect(dataCache.caches.fileContent.has(mockFilePath)).toBe(false);
expect(dataCache.caches.parsedData.has(mockFilePath)).toBe(false);
expect(dataCache.caches.computationResults.has(`tokens_${mockFilePath}`)).toBe(false);
});
});
describe('clearExpired', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('should remove expired cache entries', () => {
const now = Date.now();
jest.setSystemTime(now);
// Add expired entry
dataCache.caches.fileContent.set('/expired/file', {
content: 'old',
timestamp: now - 100000 // Older than TTL
});
// Add fresh entry
dataCache.caches.fileContent.set('/fresh/file', {
content: 'new',
timestamp: now - 1000 // Within TTL
});
// Fast forward time
jest.setSystemTime(now + 50000);
dataCache.clearExpired();
expect(dataCache.caches.fileContent.has('/expired/file')).toBe(false);
expect(dataCache.caches.fileContent.has('/fresh/file')).toBe(true);
});
});
describe('getStats', () => {
it('should return cache statistics', async () => {
// Add some test data
await dataCache.getFileContent('/test/file1.jsonl');
await dataCache.getFileContent('/test/file2.jsonl');
const stats = dataCache.getStats();
expect(stats).toMatchObject({
fileContent: expect.objectContaining({
size: expect.any(Number),
hitRate: expect.any(Number)
}),
parsedData: expect.objectContaining({
size: expect.any(Number)
}),
computationResults: expect.objectContaining({
size: expect.any(Number)
}),
metrics: expect.objectContaining({
hits: expect.any(Number),
misses: expect.any(Number),
hitRate: expect.any(Number)
})
});
});
it('should calculate hit rate correctly', async () => {
// Generate some hits and misses
await dataCache.getFileContent('/test/file.jsonl'); // miss
await dataCache.getFileContent('/test/file.jsonl'); // hit
await dataCache.getFileContent('/test/file.jsonl'); // hit
const stats = dataCache.getStats();
expect(stats.metrics.hits).toBe(2);
expect(stats.metrics.misses).toBe(1);
expect(stats.metrics.hitRate).toBeCloseTo(66.67, 1); // 2/3 * 100
});
});
describe('cleanup', () => {
it('should clear all caches and reset metrics', () => {
// Populate caches
dataCache.caches.fileContent.set('test', { data: 'test' });
dataCache.caches.parsedData.set('test', { data: 'test' });
dataCache.caches.computationResults.set('test', { data: 'test' });
dataCache.metrics.hits = 10;
dataCache.metrics.misses = 5;
dataCache.cleanup();
expect(dataCache.caches.fileContent.size).toBe(0);
expect(dataCache.caches.parsedData.size).toBe(0);
expect(dataCache.caches.computationResults.size).toBe(0);
expect(dataCache.metrics.hits).toBe(0);
expect(dataCache.metrics.misses).toBe(0);
});
});
});