UNPKG

claude-usage-tracker

Version:

Advanced analytics for Claude Code usage with cost optimization, conversation length analysis, and rate limit tracking

308 lines 17.1 kB
import { describe, it, expect, beforeEach } from "vitest"; import { ConversationLengthAnalyzer } from "./conversation-length-analytics.js"; describe("ConversationLengthAnalyzer", () => { let analyzer; beforeEach(() => { analyzer = new ConversationLengthAnalyzer(); conversationCounter = 0; // Reset counter for consistent test results }); describe("Basic Functionality", () => { it("should handle empty conversation list", () => { analyzer.loadConversations([]); const analysis = analyzer.analyzeConversationLengths(); expect(analysis.totalConversations).toBe(0); expect(analysis.projectProfiles).toHaveLength(0); expect(analysis.insights).toHaveLength(0); }); it("should categorize conversation lengths correctly", () => { const entries = createTestConversations([ { id: "quick", messageCount: 3, project: "test-project" }, { id: "medium", messageCount: 15, project: "test-project" }, { id: "deep", messageCount: 50, project: "test-project" }, { id: "marathon", messageCount: 150, project: "test-project" }, ]); analyzer.loadConversations(entries); const analysis = analyzer.analyzeConversationLengths(); expect(analysis.totalConversations).toBe(4); expect(analysis.lengthDistribution.quick).toBe(0.25); expect(analysis.lengthDistribution.medium).toBe(0.25); expect(analysis.lengthDistribution.deep).toBe(0.25); expect(analysis.lengthDistribution.marathon).toBe(0.25); }); }); describe("Project Analysis", () => { it("should analyze project-specific patterns", () => { const entries = createTestConversations([ { id: "web1", messageCount: 10, project: "web-app" }, { id: "web2", messageCount: 12, project: "web-app" }, { id: "infra1", messageCount: 80, project: "infrastructure" }, { id: "infra2", messageCount: 90, project: "infrastructure" }, ]); analyzer.loadConversations(entries); const analysis = analyzer.analyzeConversationLengths(); expect(analysis.projectProfiles).toHaveLength(2); const webProject = analysis.projectProfiles.find((p) => p.project === "web-app"); const infraProject = analysis.projectProfiles.find((p) => p.project === "infrastructure"); expect(webProject).toBeDefined(); expect(infraProject).toBeDefined(); expect(webProject.avgMessageCount).toBe(11); expect(infraProject.avgMessageCount).toBe(85); }); it("should generate project-specific recommendations", () => { const entries = createTestConversations([ // Project with many marathon conversations { id: "complex1", messageCount: 200, project: "complex-project" }, { id: "complex2", messageCount: 250, project: "complex-project" }, { id: "complex3", messageCount: 300, project: "complex-project" }, ]); analyzer.loadConversations(entries); const analysis = analyzer.analyzeConversationLengths(); const complexProject = analysis.projectProfiles.find((p) => p.project === "complex-project"); expect(complexProject.recommendations.some(rec => rec.includes('Break down complex tasks'))).toBe(true); }); }); describe("Success Pattern Analysis", () => { it("should identify quick follow-up patterns", () => { const now = new Date(); const oneHourLater = new Date(now.getTime() + 60 * 60 * 1000); const entries = [ ...createConversationWithTiming("conv1", 5, "project1", now), ...createConversationWithTiming("conv2", 3, "project1", oneHourLater), // Quick follow-up ]; analyzer.loadConversations(entries); const analysis = analyzer.analyzeConversationLengths(); // First conversation should be marked as having quick follow-up expect(analysis.recommendations.some(rec => rec.includes('quick follow-ups') && rec.includes('thorough'))).toBe(true); }); it("should detect conversation completion patterns", () => { const entries = createTestConversations([ { id: "normal", messageCount: 25, project: "test" }, { id: "stuck", messageCount: 300, project: "test" }, // Very long = likely stuck ]); analyzer.loadConversations(entries); const analysis = analyzer.analyzeConversationLengths(); // Should recommend breaking down complex problems expect(analysis.recommendations.some(rec => rec.includes('breaking down complex'))).toBe(true); }); }); describe("Efficiency Analysis", () => { it("should calculate token efficiency correctly", () => { const entries = createConversationWithTokensAndDuration("test", 1000, 10); // 100 tokens/minute analyzer.loadConversations(entries); const analysis = analyzer.analyzeConversationLengths(); expect(analysis.projectProfiles[0].efficiencyByLength.medium.avgEfficiency).toBeCloseTo(100, 1); }); it("should identify low efficiency conversations", () => { const entries = createConversationWithTokensAndDuration("slow", 100, 10); // 10 tokens/minute (low) analyzer.loadConversations(entries); const analysis = analyzer.analyzeConversationLengths(); expect(analysis.recommendations.some(rec => rec.includes('Focus conversations') && rec.includes('specific questions'))).toBe(true); }); }); describe("Optimal Range Calculation", () => { it("should calculate optimal range based on success rates", () => { // Create conversations where medium length has highest success const entries = [ ...createSuccessfulConversation("quick1", 3, "test", false), // Quick but unsuccessful ...createSuccessfulConversation("medium1", 15, "test", true), // Medium and successful ...createSuccessfulConversation("medium2", 18, "test", true), // Medium and successful ...createSuccessfulConversation("deep1", 80, "test", false), // Deep but unsuccessful ]; analyzer.loadConversations(entries); const analysis = analyzer.analyzeConversationLengths(); expect(analysis.overallOptimalRange.minMessages).toBe(6); expect(analysis.overallOptimalRange.maxMessages).toBe(20); expect(analysis.overallOptimalRange.explanation).toContain("Medium-length"); }); it("should adapt optimal range for different project types", () => { const entries = [ // Simple project - quick conversations work well ...createSuccessfulConversation("simple1", 2, "simple-project", true), ...createSuccessfulConversation("simple2", 4, "simple-project", true), // Complex project - deep conversations work better ...createSuccessfulConversation("complex1", 60, "complex-project", true), ...createSuccessfulConversation("complex2", 80, "complex-project", true), ]; analyzer.loadConversations(entries); const analysis = analyzer.analyzeConversationLengths(); const simpleProject = analysis.projectProfiles.find((p) => p.project === "simple-project"); const complexProject = analysis.projectProfiles.find((p) => p.project === "complex-project"); expect(simpleProject.optimalRange.maxMessages).toBeLessThan(complexProject.optimalRange.maxMessages); }); }); describe("Edge Cases", () => { it("should handle single message conversations", () => { const entries = createTestConversations([ { id: "single", messageCount: 1, project: "test" }, ]); analyzer.loadConversations(entries); const analysis = analyzer.analyzeConversationLengths(); expect(analysis.totalConversations).toBe(1); expect(analysis.lengthDistribution.quick).toBe(1); }); it("should handle conversations with zero duration", () => { const now = new Date(); const entries = [ createEntry("conv1", "test-project", now, 1, 100, 50, 1.0), createEntry("conv1", "test-project", now, 2, 100, 50, 1.0), // Same timestamp ]; analyzer.loadConversations(entries); const analysis = analyzer.analyzeConversationLengths(); // Should handle zero duration gracefully (minimum 1 minute) expect(analysis.projectProfiles[0].avgDuration).toBeGreaterThan(0); }); it("should handle missing cost data", () => { const entries = [ createEntry("conv1", "test", new Date(), 1, 100, 50, undefined), // No cost ]; analyzer.loadConversations(entries); const analysis = analyzer.analyzeConversationLengths(); expect(analysis.totalConversations).toBe(1); expect(analysis.projectProfiles[0].totalConversations).toBe(1); }); it("should handle very large conversation counts", () => { const entries = []; // Create 1000 conversations to test performance for (let i = 0; i < 1000; i++) { entries.push(...createTestConversations([ { id: `conv${i}`, messageCount: 10, project: "large-project" }, ])); } analyzer.loadConversations(entries); const start = Date.now(); const analysis = analyzer.analyzeConversationLengths(); const duration = Date.now() - start; expect(analysis.totalConversations).toBe(1000); expect(duration).toBeLessThan(1000); // Should complete within 1 second }); }); describe("Real-world Scenarios", () => { it("should analyze debugging vs feature development patterns", () => { const entries = [ // Debugging typically has shorter, focused conversations ...createTestConversations([ { id: "debug1", messageCount: 5, project: "debugging" }, { id: "debug2", messageCount: 8, project: "debugging" }, ]), // Feature development has longer conversations ...createTestConversations([ { id: "feature1", messageCount: 40, project: "feature-dev" }, { id: "feature2", messageCount: 35, project: "feature-dev" }, ]), ]; analyzer.loadConversations(entries); const analysis = analyzer.analyzeConversationLengths(); const debugProject = analysis.projectProfiles.find((p) => p.project === "debugging"); const featureProject = analysis.projectProfiles.find((p) => p.project === "feature-dev"); expect(debugProject.avgMessageCount).toBeLessThan(featureProject.avgMessageCount); expect(debugProject.optimalRange.maxMessages).toBeLessThan(featureProject.optimalRange.maxMessages); }); it("should identify learning vs maintenance patterns", () => { const entries = [ // Learning conversations tend to be longer ...createTestConversations([ { id: "learn1", messageCount: 60, project: "learning" }, { id: "learn2", messageCount: 45, project: "learning" }, ]), // Maintenance conversations tend to be shorter ...createTestConversations([ { id: "maint1", messageCount: 12, project: "maintenance" }, { id: "maint2", messageCount: 8, project: "maintenance" }, ]), ]; analyzer.loadConversations(entries); const analysis = analyzer.analyzeConversationLengths(); const learningProject = analysis.projectProfiles.find((p) => p.project === "learning"); const maintenanceProject = analysis.projectProfiles.find((p) => p.project === "maintenance"); expect(learningProject.avgMessageCount).toBeGreaterThan(maintenanceProject.avgMessageCount); }); }); describe("Recommendation Quality", () => { it("should provide actionable recommendations for marathon conversations", () => { const entries = createTestConversations([ { id: "marathon1", messageCount: 400, project: "complex" }, { id: "marathon2", messageCount: 300, project: "complex" }, { id: "normal", messageCount: 20, project: "complex" }, ]); analyzer.loadConversations(entries); const analysis = analyzer.analyzeConversationLengths(); expect(analysis.recommendations.some(rec => rec.includes('breaking down complex problems'))).toBe(true); }); it("should identify successful patterns to replicate", () => { const entries = [ ...createSuccessfulConversation("success1", 15, "web-dev", true), ...createSuccessfulConversation("success2", 18, "web-dev", true), ...createSuccessfulConversation("failure1", 80, "web-dev", false), ]; analyzer.loadConversations(entries); const analysis = analyzer.analyzeConversationLengths(); const webProject = analysis.projectProfiles.find((p) => p.project === "web-dev"); expect(webProject.recommendations.some(rec => rec.includes('conversations work well'))).toBe(true); }); }); }); // Helper functions for creating test data function createTestConversations(conversations) { const entries = []; const baseTime = new Date("2024-01-01T10:00:00Z"); for (const conv of conversations) { for (let i = 0; i < conv.messageCount; i++) { const timestamp = new Date(baseTime.getTime() + i * 60000); // 1 minute apart entries.push(createEntry(conv.id, conv.project, timestamp, i + 1, 100, 50, 1.0)); } } return entries; } function createConversationWithTiming(conversationId, messageCount, project, startTime) { const entries = []; for (let i = 0; i < messageCount; i++) { const timestamp = new Date(startTime.getTime() + i * 60000); // 1 minute apart entries.push(createEntry(conversationId, project, timestamp, i + 1, 100, 50, 1.0)); } return entries; } function createConversationWithTokensAndDuration(conversationId, totalTokens, durationMinutes) { const entries = []; const messageCount = 10; // Fixed message count const tokensPerMessage = totalTokens / messageCount; const baseTime = new Date(); for (let i = 0; i < messageCount; i++) { // Spread messages across the full duration, with last message at exactly durationMinutes const timestamp = new Date(baseTime.getTime() + i * ((durationMinutes * 60000) / Math.max(1, messageCount - 1))); entries.push(createEntry(conversationId, "test-project", timestamp, i + 1, tokensPerMessage / 2, tokensPerMessage / 2, 0.1)); } return entries; } let conversationCounter = 0; function createSuccessfulConversation(conversationId, messageCount, project, successful) { const entries = []; const baseTime = new Date("2024-01-01T10:00:00Z"); // Space conversations apart by 1 day each to avoid follow-up detection issues baseTime.setDate(baseTime.getDate() + conversationCounter++); for (let i = 0; i < messageCount; i++) { const timestamp = new Date(baseTime.getTime() + i * 60000); entries.push(createEntry(conversationId, project, timestamp, i + 1, 100, 50, 1.0)); } // If successful, don't create follow-up conversation // If unsuccessful, create a quick follow-up (simulates needing to continue) if (!successful) { const followUpTime = new Date(baseTime.getTime() + messageCount * 60000 + 30 * 60000); // 30 min later entries.push(createEntry(conversationId + "_followup", project, followUpTime, 1, 50, 25, 0.5)); } return entries; } function createEntry(conversationId, project, timestamp, messageNumber, inputTokens, outputTokens, cost) { return { timestamp: timestamp.toISOString(), conversationId: conversationId, instanceId: project, model: "claude-3-sonnet", requestId: `req_${conversationId}_${messageNumber}`, prompt_tokens: inputTokens, completion_tokens: outputTokens, total_tokens: inputTokens + outputTokens, cost: cost, }; } //# sourceMappingURL=conversation-length-analytics.test.js.map