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

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);
});
});
});