chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,349 @@
|
||||
/**
|
||||
* Integration Tests for Analytics System
|
||||
* Tests the complete analytics system integration
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs-extra');
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
describe('Analytics System Integration', () => {
|
||||
let analyticsProcess;
|
||||
let testDataDir;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Create test data directory
|
||||
testDataDir = path.join(__dirname, '../fixtures/test-conversations');
|
||||
await fs.ensureDir(testDataDir);
|
||||
|
||||
// Create sample conversation files
|
||||
await createTestConversationFiles(testDataDir);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Cleanup test data
|
||||
await fs.remove(testDataDir);
|
||||
|
||||
// Kill analytics process if running
|
||||
if (analyticsProcess) {
|
||||
analyticsProcess.kill('SIGTERM');
|
||||
}
|
||||
});
|
||||
|
||||
describe('Backend Integration', () => {
|
||||
it('should load and analyze conversation data correctly', async () => {
|
||||
const ClaudeAnalytics = require('../../src/analytics');
|
||||
|
||||
const analytics = new ClaudeAnalytics();
|
||||
analytics.claudeDir = testDataDir;
|
||||
|
||||
// Mock the setup methods to avoid actual server startup
|
||||
analytics.setupWebServer = jest.fn();
|
||||
analytics.setupFileWatchers = jest.fn();
|
||||
|
||||
await analytics.loadInitialData();
|
||||
|
||||
expect(analytics.data.conversations).toBeDefined();
|
||||
expect(analytics.data.conversations.length).toBeGreaterThan(0);
|
||||
expect(analytics.data.summary).toBeDefined();
|
||||
expect(analytics.data.summary.totalConversations).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should detect conversation states correctly', async () => {
|
||||
const StateCalculator = require('../../src/analytics/core/StateCalculator');
|
||||
const ConversationAnalyzer = require('../../src/analytics/core/ConversationAnalyzer');
|
||||
|
||||
const stateCalculator = new StateCalculator();
|
||||
const analyzer = new ConversationAnalyzer(testDataDir);
|
||||
|
||||
const data = await analyzer.loadInitialData(stateCalculator);
|
||||
|
||||
expect(data.conversations).toBeDefined();
|
||||
data.conversations.forEach(conv => {
|
||||
expect(['active', 'waiting', 'idle', 'completed']).toContain(conv.status);
|
||||
expect(conv.tokens).toBeGreaterThan(0);
|
||||
expect(conv.messages).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should cache data efficiently', async () => {
|
||||
const DataCache = require('../../src/analytics/data/DataCache');
|
||||
|
||||
const cache = new DataCache();
|
||||
const testFile = path.join(testDataDir, 'conversation_1.jsonl');
|
||||
|
||||
// First read - cache miss
|
||||
const start1 = Date.now();
|
||||
const content1 = await cache.getFileContent(testFile);
|
||||
const time1 = Date.now() - start1;
|
||||
|
||||
// Second read - cache hit
|
||||
const start2 = Date.now();
|
||||
const content2 = await cache.getFileContent(testFile);
|
||||
const time2 = Date.now() - start2;
|
||||
|
||||
expect(content1).toBe(content2);
|
||||
expect(time2).toBeLessThan(time1); // Cache should be faster
|
||||
expect(cache.metrics.hits).toBe(1);
|
||||
expect(cache.metrics.misses).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('WebSocket Integration', () => {
|
||||
let webSocketServer;
|
||||
let httpServer;
|
||||
let mockClient;
|
||||
|
||||
beforeEach(async () => {
|
||||
const WebSocketServer = require('../../src/analytics/notifications/WebSocketServer');
|
||||
const http = require('http');
|
||||
|
||||
httpServer = http.createServer();
|
||||
webSocketServer = new WebSocketServer(httpServer);
|
||||
|
||||
await new Promise((resolve) => {
|
||||
httpServer.listen(0, () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
await webSocketServer.initialize();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (webSocketServer) {
|
||||
await webSocketServer.close();
|
||||
}
|
||||
if (httpServer) {
|
||||
httpServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle WebSocket connections and messaging', async () => {
|
||||
const WebSocket = require('ws');
|
||||
const port = httpServer.address().port;
|
||||
|
||||
// Create WebSocket client
|
||||
mockClient = new WebSocket(`ws://localhost:${port}/ws`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('WebSocket test timeout'));
|
||||
}, 5000);
|
||||
|
||||
mockClient.on('open', () => {
|
||||
// Send subscribe message
|
||||
mockClient.send(JSON.stringify({
|
||||
type: 'subscribe',
|
||||
channel: 'conversation_updates'
|
||||
}));
|
||||
});
|
||||
|
||||
mockClient.on('message', (data) => {
|
||||
const message = JSON.parse(data);
|
||||
|
||||
if (message.type === 'connection') {
|
||||
expect(message.data.clientId).toBeDefined();
|
||||
} else if (message.type === 'subscription_confirmed') {
|
||||
expect(message.data.channel).toBe('conversation_updates');
|
||||
clearTimeout(timeout);
|
||||
mockClient.close();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
mockClient.on('error', (error) => {
|
||||
clearTimeout(timeout);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should broadcast notifications to subscribed clients', async () => {
|
||||
const NotificationManager = require('../../src/analytics/notifications/NotificationManager');
|
||||
|
||||
const notificationManager = new NotificationManager(webSocketServer);
|
||||
await notificationManager.initialize();
|
||||
|
||||
const WebSocket = require('ws');
|
||||
const port = httpServer.address().port;
|
||||
|
||||
mockClient = new WebSocket(`ws://localhost:${port}/ws`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('Notification test timeout'));
|
||||
}, 5000);
|
||||
|
||||
let subscribed = false;
|
||||
|
||||
mockClient.on('open', () => {
|
||||
mockClient.send(JSON.stringify({
|
||||
type: 'subscribe',
|
||||
channel: 'conversation_updates'
|
||||
}));
|
||||
});
|
||||
|
||||
mockClient.on('message', (data) => {
|
||||
const message = JSON.parse(data);
|
||||
|
||||
if (message.type === 'subscription_confirmed' && !subscribed) {
|
||||
subscribed = true;
|
||||
// Send notification
|
||||
notificationManager.notifyConversationStateChange(
|
||||
'conv_123', 'idle', 'active', { project: 'test' }
|
||||
);
|
||||
} else if (message.type === 'conversation_state_change') {
|
||||
expect(message.data.conversationId).toBe('conv_123');
|
||||
expect(message.data.newState).toBe('active');
|
||||
clearTimeout(timeout);
|
||||
mockClient.close();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
mockClient.on('error', (error) => {
|
||||
clearTimeout(timeout);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('End-to-End Analytics Flow', () => {
|
||||
it('should process conversation changes end-to-end', async () => {
|
||||
const ClaudeAnalytics = require('../../src/analytics');
|
||||
const analytics = new ClaudeAnalytics();
|
||||
|
||||
// Mock server setup
|
||||
analytics.setupWebServer = jest.fn();
|
||||
analytics.setupFileWatchers = jest.fn();
|
||||
analytics.claudeDir = testDataDir;
|
||||
|
||||
// Initialize analytics
|
||||
await analytics.loadInitialData();
|
||||
|
||||
const initialConversationCount = analytics.data.conversations.length;
|
||||
|
||||
// Create new conversation file
|
||||
const newConvFile = path.join(testDataDir, 'new_conversation.jsonl');
|
||||
const newConvData = {
|
||||
message: {
|
||||
role: 'user',
|
||||
content: 'New test message'
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
await fs.writeFile(newConvFile, JSON.stringify(newConvData) + '\n');
|
||||
|
||||
// Reload data
|
||||
await analytics.loadInitialData();
|
||||
|
||||
expect(analytics.data.conversations.length).toBe(initialConversationCount + 1);
|
||||
|
||||
// Cleanup
|
||||
await fs.remove(newConvFile);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Performance Tests', () => {
|
||||
it('should handle large datasets efficiently', async () => {
|
||||
const ConversationAnalyzer = require('../../src/analytics/core/ConversationAnalyzer');
|
||||
const DataCache = require('../../src/analytics/data/DataCache');
|
||||
|
||||
const cache = new DataCache();
|
||||
const analyzer = new ConversationAnalyzer(testDataDir, cache);
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
// Run analysis multiple times to test caching
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await analyzer.loadInitialData();
|
||||
}
|
||||
|
||||
const endTime = Date.now();
|
||||
const totalTime = endTime - startTime;
|
||||
|
||||
// Should complete within reasonable time (adjust based on test data size)
|
||||
expect(totalTime).toBeLessThan(10000); // 10 seconds max
|
||||
|
||||
// Cache should have improved performance
|
||||
expect(cache.metrics.hits).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should handle concurrent operations safely', async () => {
|
||||
const DataCache = require('../../src/analytics/data/DataCache');
|
||||
const cache = new DataCache();
|
||||
|
||||
const testFile = path.join(testDataDir, 'conversation_1.jsonl');
|
||||
|
||||
// Simulate concurrent file reads
|
||||
const promises = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
promises.push(cache.getFileContent(testFile));
|
||||
}
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
// All results should be identical
|
||||
const firstResult = results[0];
|
||||
results.forEach(result => {
|
||||
expect(result).toBe(firstResult);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper function to create test conversation files
|
||||
*/
|
||||
async function createTestConversationFiles(testDir) {
|
||||
const conversations = [
|
||||
{
|
||||
filename: 'conversation_1.jsonl',
|
||||
data: [
|
||||
{
|
||||
message: { role: 'user', content: 'Hello, can you help me with JavaScript?' },
|
||||
timestamp: new Date(Date.now() - 3600000).toISOString() // 1 hour ago
|
||||
},
|
||||
{
|
||||
message: { role: 'assistant', content: 'Of course! I\'d be happy to help you with JavaScript.' },
|
||||
timestamp: new Date(Date.now() - 3500000).toISOString()
|
||||
},
|
||||
{
|
||||
message: { role: 'user', content: 'How do I create a function?' },
|
||||
timestamp: new Date(Date.now() - 3000000).toISOString()
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
filename: 'conversation_2.jsonl',
|
||||
data: [
|
||||
{
|
||||
message: { role: 'user', content: 'I need help with React components' },
|
||||
timestamp: new Date(Date.now() - 7200000).toISOString() // 2 hours ago
|
||||
},
|
||||
{
|
||||
message: { role: 'assistant', content: 'React components are the building blocks of React applications.' },
|
||||
timestamp: new Date(Date.now() - 7000000).toISOString()
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
filename: 'conversation_3.jsonl',
|
||||
data: [
|
||||
{
|
||||
message: { role: 'user', content: 'Quick question about CSS' },
|
||||
timestamp: new Date(Date.now() - 60000).toISOString() // 1 minute ago
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
for (const conv of conversations) {
|
||||
const filePath = path.join(testDir, conv.filename);
|
||||
const content = conv.data.map(item => JSON.stringify(item)).join('\n') + '\n';
|
||||
await fs.writeFile(filePath, content);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user