chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user