diff options
Diffstat (limited to 'packages/cli/src/ui/utils')
| -rw-r--r-- | packages/cli/src/ui/utils/computeStats.test.ts | 247 | ||||
| -rw-r--r-- | packages/cli/src/ui/utils/computeStats.ts | 84 | ||||
| -rw-r--r-- | packages/cli/src/ui/utils/displayUtils.test.ts | 58 | ||||
| -rw-r--r-- | packages/cli/src/ui/utils/displayUtils.ts | 32 | ||||
| -rw-r--r-- | packages/cli/src/ui/utils/formatters.ts | 2 |
5 files changed, 422 insertions, 1 deletions
diff --git a/packages/cli/src/ui/utils/computeStats.test.ts b/packages/cli/src/ui/utils/computeStats.test.ts new file mode 100644 index 00000000..0e32ffe2 --- /dev/null +++ b/packages/cli/src/ui/utils/computeStats.test.ts @@ -0,0 +1,247 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect } from 'vitest'; +import { + calculateAverageLatency, + calculateCacheHitRate, + calculateErrorRate, + computeSessionStats, +} from './computeStats.js'; +import { ModelMetrics, SessionMetrics } from '../contexts/SessionContext.js'; + +describe('calculateErrorRate', () => { + it('should return 0 if totalRequests is 0', () => { + const metrics: ModelMetrics = { + api: { totalRequests: 0, totalErrors: 0, totalLatencyMs: 0 }, + tokens: { + prompt: 0, + candidates: 0, + total: 0, + cached: 0, + thoughts: 0, + tool: 0, + }, + }; + expect(calculateErrorRate(metrics)).toBe(0); + }); + + it('should calculate the error rate correctly', () => { + const metrics: ModelMetrics = { + api: { totalRequests: 10, totalErrors: 2, totalLatencyMs: 0 }, + tokens: { + prompt: 0, + candidates: 0, + total: 0, + cached: 0, + thoughts: 0, + tool: 0, + }, + }; + expect(calculateErrorRate(metrics)).toBe(20); + }); +}); + +describe('calculateAverageLatency', () => { + it('should return 0 if totalRequests is 0', () => { + const metrics: ModelMetrics = { + api: { totalRequests: 0, totalErrors: 0, totalLatencyMs: 1000 }, + tokens: { + prompt: 0, + candidates: 0, + total: 0, + cached: 0, + thoughts: 0, + tool: 0, + }, + }; + expect(calculateAverageLatency(metrics)).toBe(0); + }); + + it('should calculate the average latency correctly', () => { + const metrics: ModelMetrics = { + api: { totalRequests: 10, totalErrors: 0, totalLatencyMs: 1500 }, + tokens: { + prompt: 0, + candidates: 0, + total: 0, + cached: 0, + thoughts: 0, + tool: 0, + }, + }; + expect(calculateAverageLatency(metrics)).toBe(150); + }); +}); + +describe('calculateCacheHitRate', () => { + it('should return 0 if prompt tokens is 0', () => { + const metrics: ModelMetrics = { + api: { totalRequests: 0, totalErrors: 0, totalLatencyMs: 0 }, + tokens: { + prompt: 0, + candidates: 0, + total: 0, + cached: 100, + thoughts: 0, + tool: 0, + }, + }; + expect(calculateCacheHitRate(metrics)).toBe(0); + }); + + it('should calculate the cache hit rate correctly', () => { + const metrics: ModelMetrics = { + api: { totalRequests: 0, totalErrors: 0, totalLatencyMs: 0 }, + tokens: { + prompt: 200, + candidates: 0, + total: 0, + cached: 50, + thoughts: 0, + tool: 0, + }, + }; + expect(calculateCacheHitRate(metrics)).toBe(25); + }); +}); + +describe('computeSessionStats', () => { + it('should return all zeros for initial empty metrics', () => { + const metrics: SessionMetrics = { + models: {}, + tools: { + totalCalls: 0, + totalSuccess: 0, + totalFail: 0, + totalDurationMs: 0, + totalDecisions: { accept: 0, reject: 0, modify: 0 }, + byName: {}, + }, + }; + + const result = computeSessionStats(metrics); + + expect(result).toEqual({ + totalApiTime: 0, + totalToolTime: 0, + agentActiveTime: 0, + apiTimePercent: 0, + toolTimePercent: 0, + cacheEfficiency: 0, + totalDecisions: 0, + successRate: 0, + agreementRate: 0, + totalPromptTokens: 0, + totalCachedTokens: 0, + }); + }); + + it('should correctly calculate API and tool time percentages', () => { + const metrics: SessionMetrics = { + models: { + 'gemini-pro': { + api: { totalRequests: 1, totalErrors: 0, totalLatencyMs: 750 }, + tokens: { + prompt: 10, + candidates: 10, + total: 20, + cached: 0, + thoughts: 0, + tool: 0, + }, + }, + }, + tools: { + totalCalls: 1, + totalSuccess: 1, + totalFail: 0, + totalDurationMs: 250, + totalDecisions: { accept: 0, reject: 0, modify: 0 }, + byName: {}, + }, + }; + + const result = computeSessionStats(metrics); + + expect(result.totalApiTime).toBe(750); + expect(result.totalToolTime).toBe(250); + expect(result.agentActiveTime).toBe(1000); + expect(result.apiTimePercent).toBe(75); + expect(result.toolTimePercent).toBe(25); + }); + + it('should correctly calculate cache efficiency', () => { + const metrics: SessionMetrics = { + models: { + 'gemini-pro': { + api: { totalRequests: 2, totalErrors: 0, totalLatencyMs: 1000 }, + tokens: { + prompt: 150, + candidates: 10, + total: 160, + cached: 50, + thoughts: 0, + tool: 0, + }, + }, + }, + tools: { + totalCalls: 0, + totalSuccess: 0, + totalFail: 0, + totalDurationMs: 0, + totalDecisions: { accept: 0, reject: 0, modify: 0 }, + byName: {}, + }, + }; + + const result = computeSessionStats(metrics); + + expect(result.cacheEfficiency).toBeCloseTo(33.33); // 50 / 150 + }); + + it('should correctly calculate success and agreement rates', () => { + const metrics: SessionMetrics = { + models: {}, + tools: { + totalCalls: 10, + totalSuccess: 8, + totalFail: 2, + totalDurationMs: 1000, + totalDecisions: { accept: 6, reject: 2, modify: 2 }, + byName: {}, + }, + }; + + const result = computeSessionStats(metrics); + + expect(result.successRate).toBe(80); // 8 / 10 + expect(result.agreementRate).toBe(60); // 6 / 10 + }); + + it('should handle division by zero gracefully', () => { + const metrics: SessionMetrics = { + models: {}, + tools: { + totalCalls: 0, + totalSuccess: 0, + totalFail: 0, + totalDurationMs: 0, + totalDecisions: { accept: 0, reject: 0, modify: 0 }, + byName: {}, + }, + }; + + const result = computeSessionStats(metrics); + + expect(result.apiTimePercent).toBe(0); + expect(result.toolTimePercent).toBe(0); + expect(result.cacheEfficiency).toBe(0); + expect(result.successRate).toBe(0); + expect(result.agreementRate).toBe(0); + }); +}); diff --git a/packages/cli/src/ui/utils/computeStats.ts b/packages/cli/src/ui/utils/computeStats.ts new file mode 100644 index 00000000..e0483c3b --- /dev/null +++ b/packages/cli/src/ui/utils/computeStats.ts @@ -0,0 +1,84 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + SessionMetrics, + ComputedSessionStats, + ModelMetrics, +} from '../contexts/SessionContext.js'; + +export function calculateErrorRate(metrics: ModelMetrics): number { + if (metrics.api.totalRequests === 0) { + return 0; + } + return (metrics.api.totalErrors / metrics.api.totalRequests) * 100; +} + +export function calculateAverageLatency(metrics: ModelMetrics): number { + if (metrics.api.totalRequests === 0) { + return 0; + } + return metrics.api.totalLatencyMs / metrics.api.totalRequests; +} + +export function calculateCacheHitRate(metrics: ModelMetrics): number { + if (metrics.tokens.prompt === 0) { + return 0; + } + return (metrics.tokens.cached / metrics.tokens.prompt) * 100; +} + +export const computeSessionStats = ( + metrics: SessionMetrics, +): ComputedSessionStats => { + const { models, tools } = metrics; + const totalApiTime = Object.values(models).reduce( + (acc, model) => acc + model.api.totalLatencyMs, + 0, + ); + const totalToolTime = tools.totalDurationMs; + const agentActiveTime = totalApiTime + totalToolTime; + const apiTimePercent = + agentActiveTime > 0 ? (totalApiTime / agentActiveTime) * 100 : 0; + const toolTimePercent = + agentActiveTime > 0 ? (totalToolTime / agentActiveTime) * 100 : 0; + + const totalCachedTokens = Object.values(models).reduce( + (acc, model) => acc + model.tokens.cached, + 0, + ); + const totalPromptTokens = Object.values(models).reduce( + (acc, model) => acc + model.tokens.prompt, + 0, + ); + const cacheEfficiency = + totalPromptTokens > 0 ? (totalCachedTokens / totalPromptTokens) * 100 : 0; + + const totalDecisions = + tools.totalDecisions.accept + + tools.totalDecisions.reject + + tools.totalDecisions.modify; + const successRate = + tools.totalCalls > 0 ? (tools.totalSuccess / tools.totalCalls) * 100 : 0; + const agreementRate = + totalDecisions > 0 + ? (tools.totalDecisions.accept / totalDecisions) * 100 + : 0; + + return { + totalApiTime, + totalToolTime, + agentActiveTime, + apiTimePercent, + toolTimePercent, + cacheEfficiency, + totalDecisions, + successRate, + agreementRate, + totalCachedTokens, + totalPromptTokens, + }; +}; diff --git a/packages/cli/src/ui/utils/displayUtils.test.ts b/packages/cli/src/ui/utils/displayUtils.test.ts new file mode 100644 index 00000000..7dd9f0e8 --- /dev/null +++ b/packages/cli/src/ui/utils/displayUtils.test.ts @@ -0,0 +1,58 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect } from 'vitest'; +import { + getStatusColor, + TOOL_SUCCESS_RATE_HIGH, + TOOL_SUCCESS_RATE_MEDIUM, + USER_AGREEMENT_RATE_HIGH, + USER_AGREEMENT_RATE_MEDIUM, + CACHE_EFFICIENCY_HIGH, + CACHE_EFFICIENCY_MEDIUM, +} from './displayUtils.js'; +import { Colors } from '../colors.js'; + +describe('displayUtils', () => { + describe('getStatusColor', () => { + const thresholds = { + green: 80, + yellow: 50, + }; + + it('should return green for values >= green threshold', () => { + expect(getStatusColor(90, thresholds)).toBe(Colors.AccentGreen); + expect(getStatusColor(80, thresholds)).toBe(Colors.AccentGreen); + }); + + it('should return yellow for values < green and >= yellow threshold', () => { + expect(getStatusColor(79, thresholds)).toBe(Colors.AccentYellow); + expect(getStatusColor(50, thresholds)).toBe(Colors.AccentYellow); + }); + + it('should return red for values < yellow threshold', () => { + expect(getStatusColor(49, thresholds)).toBe(Colors.AccentRed); + expect(getStatusColor(0, thresholds)).toBe(Colors.AccentRed); + }); + + it('should return defaultColor for values < yellow threshold when provided', () => { + expect( + getStatusColor(49, thresholds, { defaultColor: Colors.Foreground }), + ).toBe(Colors.Foreground); + }); + }); + + describe('Threshold Constants', () => { + it('should have the correct values', () => { + expect(TOOL_SUCCESS_RATE_HIGH).toBe(95); + expect(TOOL_SUCCESS_RATE_MEDIUM).toBe(85); + expect(USER_AGREEMENT_RATE_HIGH).toBe(75); + expect(USER_AGREEMENT_RATE_MEDIUM).toBe(45); + expect(CACHE_EFFICIENCY_HIGH).toBe(40); + expect(CACHE_EFFICIENCY_MEDIUM).toBe(15); + }); + }); +}); diff --git a/packages/cli/src/ui/utils/displayUtils.ts b/packages/cli/src/ui/utils/displayUtils.ts new file mode 100644 index 00000000..a52c6ff0 --- /dev/null +++ b/packages/cli/src/ui/utils/displayUtils.ts @@ -0,0 +1,32 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Colors } from '../colors.js'; + +// --- Thresholds --- +export const TOOL_SUCCESS_RATE_HIGH = 95; +export const TOOL_SUCCESS_RATE_MEDIUM = 85; + +export const USER_AGREEMENT_RATE_HIGH = 75; +export const USER_AGREEMENT_RATE_MEDIUM = 45; + +export const CACHE_EFFICIENCY_HIGH = 40; +export const CACHE_EFFICIENCY_MEDIUM = 15; + +// --- Color Logic --- +export const getStatusColor = ( + value: number, + thresholds: { green: number; yellow: number }, + options: { defaultColor?: string } = {}, +) => { + if (value >= thresholds.green) { + return Colors.AccentGreen; + } + if (value >= thresholds.yellow) { + return Colors.AccentYellow; + } + return options.defaultColor || Colors.AccentRed; +}; diff --git a/packages/cli/src/ui/utils/formatters.ts b/packages/cli/src/ui/utils/formatters.ts index 82a78109..2b6af545 100644 --- a/packages/cli/src/ui/utils/formatters.ts +++ b/packages/cli/src/ui/utils/formatters.ts @@ -27,7 +27,7 @@ export const formatDuration = (milliseconds: number): string => { } if (milliseconds < 1000) { - return `${milliseconds}ms`; + return `${Math.round(milliseconds)}ms`; } const totalSeconds = milliseconds / 1000; |
