diff --git a/ui/src/components/analytics/index.ts b/ui/src/components/analytics/index.ts
index 8a5bf17d..8ebf9b39 100644
--- a/ui/src/components/analytics/index.ts
+++ b/ui/src/components/analytics/index.ts
@@ -12,3 +12,4 @@ export { TokenBreakdownChart } from './token-breakdown-chart';
export { UsageInsightsCard } from './usage-insights-card';
export { UsageSummaryCards } from './usage-summary-cards';
export { UsageTrendChart } from './usage-trend-chart';
+export { getProjectDisplayName } from './project-name-utils';
diff --git a/ui/src/components/analytics/project-name-utils.ts b/ui/src/components/analytics/project-name-utils.ts
index 7e475337..c3d887e2 100644
--- a/ui/src/components/analytics/project-name-utils.ts
+++ b/ui/src/components/analytics/project-name-utils.ts
@@ -23,8 +23,8 @@ export function getProjectDisplayName(path: string): string {
// Remove leading/trailing slashes and split into segments
const cleanPath = path.replace(/^\/|\/$/g, '');
- const segments = cleanPath.split('/').filter(segment => segment.length > 0);
+ const segments = cleanPath.split('/').filter((segment) => segment.length > 0);
// Return the last segment (leaf folder name)
return segments[segments.length - 1] || '';
-}
\ No newline at end of file
+}
diff --git a/ui/src/components/analytics/session-stats-card.tsx b/ui/src/components/analytics/session-stats-card.tsx
index 2ebb787e..11406ed1 100644
--- a/ui/src/components/analytics/session-stats-card.tsx
+++ b/ui/src/components/analytics/session-stats-card.tsx
@@ -153,7 +153,6 @@ export function SessionStatsCard({ data, isLoading, className }: SessionStatsCar
);
}
-
function formatCompact(num: number): string {
if (num >= 1_000_000_000) return `${(num / 1_000_000_000).toFixed(1)}B`;
if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(1)}M`;
diff --git a/ui/tests/unit/components/analytics/project-name-display.test.ts b/ui/tests/unit/components/analytics/project-name-display.test.ts
index 163a0b44..bf6477d1 100644
--- a/ui/tests/unit/components/analytics/project-name-display.test.ts
+++ b/ui/tests/unit/components/analytics/project-name-display.test.ts
@@ -6,7 +6,7 @@
import { describe, it, expect } from 'vitest';
// Import the function from the utility
-import { getProjectDisplayName } from '../../../../../src/components/analytics/project-name-utils';
+import { getProjectDisplayName } from '../../../../src/components/analytics/project-name-utils';
describe('getProjectDisplayName', () => {
describe('Simple project paths', () => {
@@ -24,13 +24,21 @@ describe('getProjectDisplayName', () => {
describe('Complex project paths', () => {
it('returns leaf folder for worktree paths', () => {
- expect(getProjectDisplayName('/Users/joe/Developer/ExaDev/Clients/Architect/repositories/architect/worktrees/2026-01-08')).toBe('2026-01-08');
- expect(getProjectDisplayName('/home/user/workspaces/repo-name/worktrees/feature-branch')).toBe('feature-branch');
+ expect(
+ getProjectDisplayName(
+ '/Users/joe/Developer/ExaDev/Clients/Architect/repositories/architect/worktrees/2026-01-08'
+ )
+ ).toBe('2026-01-08');
+ expect(
+ getProjectDisplayName('/home/user/workspaces/repo-name/worktrees/feature-branch')
+ ).toBe('feature-branch');
expect(getProjectDisplayName('/project/repo/worktrees/v2.0')).toBe('v2.0');
});
it('handles nested paths', () => {
- expect(getProjectDisplayName('/home/user/projects/web-dashboard/src/components')).toBe('components');
+ expect(getProjectDisplayName('/home/user/projects/web-dashboard/src/components')).toBe(
+ 'components'
+ );
expect(getProjectDisplayName('/opt/apps/my-app/lib/utils')).toBe('utils');
});
});
@@ -72,7 +80,8 @@ describe('getProjectDisplayName', () => {
it('displays correct project name for worktree project', () => {
// Before fix: would show "08"
// After fix: should show "2026-01-08"
- const path = '/Users/joe/Developer/ExaDev/Clients/Architect/repositories/architect.worktrees/2026-01-08';
+ const path =
+ '/Users/joe/Developer/ExaDev/Clients/Architect/repositories/architect.worktrees/2026-01-08';
expect(getProjectDisplayName(path)).toBe('2026-01-08');
});
@@ -97,14 +106,9 @@ describe('getProjectDisplayName', () => {
describe('Regression tests', () => {
it('does not return empty string for valid paths', () => {
- const testCases = [
- '/project',
- '/home/user/app',
- '/var/log/nginx',
- '/tmp/test-file',
- ];
+ const testCases = ['/project', '/home/user/app', '/var/log/nginx', '/tmp/test-file'];
- testCases.forEach(path => {
+ testCases.forEach((path) => {
const result = getProjectDisplayName(path);
expect(result).not.toBe('');
expect(result).not.toBeUndefined();
@@ -124,4 +128,4 @@ describe('getProjectDisplayName', () => {
expect(getProjectDisplayName('/home/user/v1.2.3')).toBe('v1.2.3');
});
});
-});
\ No newline at end of file
+});
diff --git a/ui/tests/unit/components/analytics/session-stats-card.test.tsx b/ui/tests/unit/components/analytics/session-stats-card.test.tsx
index e4e2508c..e09776c1 100644
--- a/ui/tests/unit/components/analytics/session-stats-card.test.tsx
+++ b/ui/tests/unit/components/analytics/session-stats-card.test.tsx
@@ -5,17 +5,16 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
-import { SessionStatsCard } from '../../../../../src/components/analytics/session-stats-card';
+import { SessionStatsCard } from '../../../../src/components/analytics/session-stats-card';
import { AllProviders } from '../../../setup/test-utils';
-import type { PaginatedSessions } from '../../../../../src/hooks/use-usage';
+import type { PaginatedSessions } from '../../../../src/hooks/use-usage';
// Mock date-fns to return consistent dates
-const mockFormatDistanceToNow = vi.fn();
vi.mock('date-fns', async () => {
const actual = await vi.importActual('date-fns');
return {
...actual,
- formatDistanceToNow: mockFormatDistanceToNow,
+ formatDistanceToNow: vi.fn(() => '27 minutes ago'),
};
});
@@ -23,17 +22,16 @@ describe('SessionStatsCard', () => {
beforeEach(() => {
// Reset all mocks
vi.clearAllMocks();
-
- // Mock formatDistanceToNow to return consistent values
- mockFormatDistanceToNow.mockReturnValue('27 minutes ago');
});
describe('Loading and Empty States', () => {
it('renders loading skeleton when isLoading is true', () => {
- render(, { wrapper: AllProviders });
+ const { container } = render(, {
+ wrapper: AllProviders,
+ });
- // Should have card structure with skeleton loading state
- expect(screen.getByRole('generic')).toBeInTheDocument();
+ // Should have skeleton loading elements
+ expect(container.querySelector('[data-slot="skeleton"]')).toBeInTheDocument();
});
it('shows empty state when no data available', () => {
@@ -59,7 +57,12 @@ describe('SessionStatsCard', () => {
});
describe('Session Stats Display', () => {
- const createMockSession = (projectPath: string, inputTokens: number, outputTokens: number, cost: number) => ({
+ const createMockSession = (
+ projectPath: string,
+ inputTokens: number,
+ outputTokens: number,
+ cost: number
+ ) => ({
sessionId: `session-${Math.random()}`,
projectPath,
inputTokens,
@@ -71,7 +74,12 @@ describe('SessionStatsCard', () => {
const mockData: PaginatedSessions = {
sessions: [
createMockSession('/home/user/projects/my-app', 1500, 2500, 0.08),
- createMockSession('/home/user/workspaces/repo-name/worktrees/feature-branch', 2000, 3000, 0.12),
+ createMockSession(
+ '/home/user/workspaces/repo-name/worktrees/feature-branch',
+ 2000,
+ 3000,
+ 0.12
+ ),
createMockSession('/Users/joe/Developer/share-pi', 1000, 2000, 0.05),
],
total: 3,
@@ -82,7 +90,12 @@ describe('SessionStatsCard', () => {
beforeEach(() => {
mockData.sessions = [
createMockSession('/home/user/projects/my-app', 1500, 2500, 0.08),
- createMockSession('/home/user/workspaces/repo-name/worktrees/feature-branch', 2000, 3000, 0.12),
+ createMockSession(
+ '/home/user/workspaces/repo-name/worktrees/feature-branch',
+ 2000,
+ 3000,
+ 0.12
+ ),
createMockSession('/Users/joe/Developer/share-pi', 1000, 2000, 0.05),
];
});
@@ -104,7 +117,9 @@ describe('SessionStatsCard', () => {
render(, { wrapper: AllProviders });
// Average cost: (0.08 + 0.12 + 0.05) / 3 = 0.0833 → $0.08
- expect(screen.getByText('$0.08')).toBeInTheDocument();
+ // Use getAllByText since cost may appear multiple times (per session + average)
+ const costElements = screen.getAllByText('$0.08');
+ expect(costElements.length).toBeGreaterThan(0);
expect(screen.getByText('Avg Cost/Session')).toBeInTheDocument();
});
@@ -126,7 +141,7 @@ describe('SessionStatsCard', () => {
outputTokens: 2000,
cost: 0.05,
lastActivity: new Date().toISOString(),
- }
+ },
],
total: 1,
page: 1,
@@ -144,12 +159,13 @@ describe('SessionStatsCard', () => {
sessions: [
{
sessionId: '1',
- projectPath: '/Users/joe/Developer/ExaDev/Clients/Architect/repositories/architect/worktrees/2026-01-08',
+ projectPath:
+ '/Users/joe/Developer/ExaDev/Clients/Architect/repositories/architect/worktrees/2026-01-08',
inputTokens: 1000,
outputTokens: 2000,
cost: 0.05,
lastActivity: new Date().toISOString(),
- }
+ },
],
total: 1,
page: 1,
@@ -159,8 +175,11 @@ describe('SessionStatsCard', () => {
render(, { wrapper: AllProviders });
// Should show "2026-01-08" instead of just "08"
- expect(screen.getByTitle('/Users/joe/Developer/ExaDev/Clients/Architect/repositories/architect/worktrees/2026-01-08'))
- .toHaveTextContent('2026-01-08');
+ expect(
+ screen.getByTitle(
+ '/Users/joe/Developer/ExaDev/Clients/Architect/repositories/architect/worktrees/2026-01-08'
+ )
+ ).toHaveTextContent('2026-01-08');
});
it('displays correct project name for shared project', () => {
@@ -173,7 +192,7 @@ describe('SessionStatsCard', () => {
outputTokens: 2000,
cost: 0.05,
lastActivity: new Date().toISOString(),
- }
+ },
],
total: 1,
page: 1,
@@ -196,7 +215,7 @@ describe('SessionStatsCard', () => {
outputTokens: 2000,
cost: 0.05,
lastActivity: new Date().toISOString(),
- }
+ },
],
total: 1,
page: 1,
@@ -219,7 +238,7 @@ describe('SessionStatsCard', () => {
outputTokens: 2000,
cost: 0.05,
lastActivity: new Date().toISOString(),
- }
+ },
],
total: 1,
page: 1,
@@ -241,10 +260,10 @@ describe('SessionStatsCard', () => {
sessionId: '1',
projectPath: '/project/test',
inputTokens: 1500000, // 1.5M
- outputTokens: 500000, // 500K
- cost: 0.10,
+ outputTokens: 500000, // 500K
+ cost: 0.1,
lastActivity: new Date().toISOString(),
- }
+ },
],
total: 1,
page: 1,
@@ -261,13 +280,26 @@ describe('SessionStatsCard', () => {
it('blurs cost information when privacy mode is enabled', () => {
// This would require mocking the privacy context
// For now, just ensure the component renders with privacy mode
- const { container } = render(
- ,
- { wrapper: AllProviders }
- );
+ const testData: PaginatedSessions = {
+ sessions: [
+ {
+ sessionId: '1',
+ projectPath: '/home/user/project',
+ inputTokens: 1000,
+ outputTokens: 2000,
+ cost: 0.05,
+ lastActivity: new Date().toISOString(),
+ },
+ ],
+ total: 1,
+ page: 1,
+ pageSize: 10,
+ };
+
+ const { container } = render(, { wrapper: AllProviders });
// Component should render without errors
expect(container).toBeInTheDocument();
});
});
-});
\ No newline at end of file
+});