253 lines
8.4 KiB
JavaScript
253 lines
8.4 KiB
JavaScript
/**
|
|
* Unit Tests for StateCalculator
|
|
* Tests conversation state detection logic
|
|
*/
|
|
|
|
const StateCalculator = require('../../src/analytics/core/StateCalculator');
|
|
|
|
describe('StateCalculator', () => {
|
|
let stateCalculator;
|
|
|
|
beforeEach(() => {
|
|
stateCalculator = new StateCalculator();
|
|
});
|
|
|
|
describe('determineConversationStatus', () => {
|
|
it('should return "active" for recent messages with user input', () => {
|
|
const messages = [
|
|
{
|
|
role: 'user',
|
|
timestamp: new Date(Date.now() - 30000), // 30 seconds ago
|
|
content: 'Hello'
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
timestamp: new Date(Date.now() - 20000), // 20 seconds ago
|
|
content: 'Hi there!'
|
|
}
|
|
];
|
|
const lastModified = new Date(Date.now() - 10000); // 10 seconds ago
|
|
|
|
const status = stateCalculator.determineConversationStatus(messages, lastModified);
|
|
expect(status).toBe('active');
|
|
});
|
|
|
|
it('should return "waiting" for conversation with recent user message', () => {
|
|
const messages = [
|
|
{
|
|
role: 'user',
|
|
timestamp: new Date(Date.now() - 30000),
|
|
content: 'Can you help me?'
|
|
}
|
|
];
|
|
const lastModified = new Date(Date.now() - 25000);
|
|
|
|
const status = stateCalculator.determineConversationStatus(messages, lastModified);
|
|
expect(status).toBe('waiting');
|
|
});
|
|
|
|
it('should return "idle" for old conversations', () => {
|
|
const messages = [
|
|
{
|
|
role: 'user',
|
|
timestamp: new Date(Date.now() - 3700000), // Over 1 hour ago
|
|
content: 'Old message'
|
|
}
|
|
];
|
|
const lastModified = new Date(Date.now() - 3600000); // 1 hour ago
|
|
|
|
const status = stateCalculator.determineConversationStatus(messages, lastModified);
|
|
expect(status).toBe('idle');
|
|
});
|
|
|
|
it('should return "completed" for conversation ending with assistant', () => {
|
|
const messages = [
|
|
{
|
|
role: 'user',
|
|
timestamp: new Date(Date.now() - 600000), // 10 minutes ago
|
|
content: 'Thanks for the help!'
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
timestamp: new Date(Date.now() - 590000), // 9:50 minutes ago
|
|
content: 'You\'re welcome! Let me know if you need anything else.'
|
|
}
|
|
];
|
|
const lastModified = new Date(Date.now() - 580000); // 9:40 minutes ago
|
|
|
|
const status = stateCalculator.determineConversationStatus(messages, lastModified);
|
|
expect(status).toBe('completed');
|
|
});
|
|
|
|
it('should handle empty messages array', () => {
|
|
const status = stateCalculator.determineConversationStatus([], new Date());
|
|
expect(status).toBe('idle');
|
|
});
|
|
|
|
it('should handle null/undefined inputs gracefully', () => {
|
|
expect(stateCalculator.determineConversationStatus(null, new Date())).toBe('idle');
|
|
expect(stateCalculator.determineConversationStatus([], null)).toBe('idle');
|
|
});
|
|
});
|
|
|
|
describe('determineConversationState', () => {
|
|
it('should return detailed state with running process', () => {
|
|
const messages = [
|
|
{
|
|
role: 'user',
|
|
timestamp: new Date(Date.now() - 30000),
|
|
content: 'Start coding'
|
|
}
|
|
];
|
|
const lastModified = new Date(Date.now() - 20000);
|
|
const runningProcess = { pid: 12345, command: 'node app.js' };
|
|
|
|
const state = stateCalculator.determineConversationState(messages, lastModified, runningProcess);
|
|
|
|
expect(state).toMatchObject({
|
|
status: 'active',
|
|
hasRunningProcess: true,
|
|
lastActivity: expect.any(Date),
|
|
messageCount: 1,
|
|
lastUserMessage: expect.any(Date),
|
|
lastAssistantMessage: null,
|
|
timeSinceLastActivity: expect.any(Number),
|
|
isRecent: true
|
|
});
|
|
});
|
|
|
|
it('should detect conversation patterns correctly', () => {
|
|
const messages = [
|
|
{
|
|
role: 'user',
|
|
timestamp: new Date(Date.now() - 120000), // 2 minutes ago
|
|
content: 'Can you help me debug this?'
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
timestamp: new Date(Date.now() - 110000), // 1:50 minutes ago
|
|
content: 'Sure! Let me take a look.'
|
|
},
|
|
{
|
|
role: 'user',
|
|
timestamp: new Date(Date.now() - 100000), // 1:40 minutes ago
|
|
content: 'Here\'s the error message...'
|
|
}
|
|
];
|
|
const lastModified = new Date(Date.now() - 90000);
|
|
|
|
const state = stateCalculator.determineConversationState(messages, lastModified);
|
|
|
|
expect(state.status).toBe('waiting');
|
|
expect(state.messageCount).toBe(3);
|
|
expect(state.lastUserMessage).toBeInstanceOf(Date);
|
|
expect(state.lastAssistantMessage).toBeInstanceOf(Date);
|
|
expect(state.isRecent).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('quickStateCalculation', () => {
|
|
const mockConversation = {
|
|
id: 'conv_123',
|
|
filePath: '/path/to/conv.jsonl',
|
|
lastModified: new Date(Date.now() - 60000).toISOString(),
|
|
status: 'idle'
|
|
};
|
|
|
|
it('should update status when running process matches conversation', () => {
|
|
const runningProcesses = [
|
|
{ pid: 123, command: 'node app.js', cwd: '/path/to' }
|
|
];
|
|
|
|
const result = stateCalculator.quickStateCalculation(mockConversation, runningProcesses);
|
|
|
|
expect(result.status).toBe('active');
|
|
expect(result.hasRunningProcess).toBe(true);
|
|
expect(result.runningProcess).toMatchObject({
|
|
pid: 123,
|
|
command: 'node app.js'
|
|
});
|
|
});
|
|
|
|
it('should keep original status when no matching process', () => {
|
|
const runningProcesses = [
|
|
{ pid: 456, command: 'other-app', cwd: '/other/path' }
|
|
];
|
|
|
|
const result = stateCalculator.quickStateCalculation(mockConversation, runningProcesses);
|
|
|
|
expect(result.status).toBe('idle');
|
|
expect(result.hasRunningProcess).toBe(false);
|
|
});
|
|
|
|
it('should handle empty processes array', () => {
|
|
const result = stateCalculator.quickStateCalculation(mockConversation, []);
|
|
|
|
expect(result.status).toBe('idle');
|
|
expect(result.hasRunningProcess).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('isRecentActivity', () => {
|
|
it('should return true for recent timestamps', () => {
|
|
const recentTime = new Date(Date.now() - 30000); // 30 seconds ago
|
|
expect(stateCalculator.isRecentActivity(recentTime)).toBe(true);
|
|
});
|
|
|
|
it('should return false for old timestamps', () => {
|
|
const oldTime = new Date(Date.now() - 3700000); // Over 1 hour ago
|
|
expect(stateCalculator.isRecentActivity(oldTime)).toBe(false);
|
|
});
|
|
|
|
it('should handle string timestamps', () => {
|
|
const recentTimeStr = new Date(Date.now() - 30000).toISOString();
|
|
expect(stateCalculator.isRecentActivity(recentTimeStr)).toBe(true);
|
|
});
|
|
|
|
it('should handle invalid timestamps', () => {
|
|
expect(stateCalculator.isRecentActivity(null)).toBe(false);
|
|
expect(stateCalculator.isRecentActivity('invalid')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('getTimeSinceLastActivity', () => {
|
|
it('should calculate time difference correctly', () => {
|
|
const timestamp = new Date(Date.now() - 60000); // 1 minute ago
|
|
const timeSince = stateCalculator.getTimeSinceLastActivity(timestamp);
|
|
|
|
expect(timeSince).toBeGreaterThan(59000); // At least 59 seconds
|
|
expect(timeSince).toBeLessThan(65000); // Less than 65 seconds
|
|
});
|
|
|
|
it('should return 0 for invalid timestamps', () => {
|
|
expect(stateCalculator.getTimeSinceLastActivity(null)).toBe(0);
|
|
expect(stateCalculator.getTimeSinceLastActivity('invalid')).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('edge cases and error handling', () => {
|
|
it('should handle malformed message objects', () => {
|
|
const messages = [
|
|
{ role: 'user' }, // Missing timestamp
|
|
{ timestamp: new Date() }, // Missing role
|
|
null, // Null message
|
|
undefined // Undefined message
|
|
];
|
|
|
|
const status = stateCalculator.determineConversationStatus(messages, new Date());
|
|
expect(status).toBe('idle'); // Should fallback gracefully
|
|
});
|
|
|
|
it('should handle timezone differences', () => {
|
|
const utcTime = new Date().toISOString();
|
|
const localTime = new Date();
|
|
|
|
const status1 = stateCalculator.determineConversationStatus(
|
|
[{ role: 'user', timestamp: utcTime, content: 'test' }],
|
|
localTime
|
|
);
|
|
|
|
expect(['active', 'waiting', 'idle', 'completed']).toContain(status1);
|
|
});
|
|
});
|
|
}); |