mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 20:14:34 +00:00
Merge pull request #362 from kaitranntt/dev
fix: GLM model version mismatch and session stats display
This commit is contained in:
14 files changed
+490
-18
No files matched your search
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "@kaitranntt/ccs",
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.z.ai/api/anthropic",
|
||||
"ANTHROPIC_AUTH_TOKEN": "YOUR_GLM_API_KEY_HERE",
|
||||
"ANTHROPIC_MODEL": "glm-4.6",
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL": "glm-4.6",
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL": "glm-4.6",
|
||||
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "glm-4.6"
|
||||
"ANTHROPIC_MODEL": "glm-4.7",
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL": "glm-4.7",
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL": "glm-4.7",
|
||||
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "glm-4.7"
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,10 @@
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.z.ai/api/coding/paas/v4/chat/completions",
|
||||
"ANTHROPIC_AUTH_TOKEN": "YOUR_GLM_API_KEY_HERE",
|
||||
"ANTHROPIC_MODEL": "glm-4.6",
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL": "glm-4.6",
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL": "glm-4.6",
|
||||
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "glm-4.6",
|
||||
"ANTHROPIC_MODEL": "glm-4.7",
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL": "glm-4.7",
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL": "glm-4.7",
|
||||
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "glm-4.7",
|
||||
"ANTHROPIC_TEMPERATURE": "0.2",
|
||||
"ANTHROPIC_MAX_TOKENS": "65536",
|
||||
"MAX_THINKING_TOKENS": "32768",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kaitranntt/ccs",
|
||||
"version": "7.26.1",
|
||||
"version": "7.26.1-dev.2",
|
||||
"description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6",
|
||||
"keywords": [
|
||||
"cli",
|
||||
|
||||
+3
-1
@@ -153,10 +153,12 @@ async function execClaudeWithProxy(
|
||||
}
|
||||
|
||||
// 4. Spawn Claude CLI with proxy URL
|
||||
// Use model from user's settings (not hardcoded) - fixes issue #358
|
||||
const configuredModel = envData['ANTHROPIC_MODEL'] || 'glm-4.7';
|
||||
const envVars: NodeJS.ProcessEnv = {
|
||||
ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}`,
|
||||
ANTHROPIC_AUTH_TOKEN: apiKey,
|
||||
ANTHROPIC_MODEL: 'glm-4.6',
|
||||
ANTHROPIC_MODEL: configuredModel,
|
||||
};
|
||||
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
@@ -113,7 +113,7 @@ export class GlmtTransformer {
|
||||
type: 'message',
|
||||
role: 'assistant',
|
||||
content,
|
||||
model: openaiResponse.model || 'glm-4.6',
|
||||
model: openaiResponse.model || 'glm-4.7',
|
||||
stop_reason: this.responseBuilder.mapStopReason(choice.finish_reason || 'stop'),
|
||||
usage: {
|
||||
input_tokens: openaiResponse.usage?.prompt_tokens || 0,
|
||||
@@ -131,7 +131,7 @@ export class GlmtTransformer {
|
||||
type: 'message',
|
||||
role: 'assistant',
|
||||
content: [{ type: 'text', text: '[Transformation Error] ' + err.message }],
|
||||
model: 'glm-4.6',
|
||||
model: 'glm-4.7',
|
||||
stop_reason: 'end_turn',
|
||||
usage: { input_tokens: 0, output_tokens: 0 },
|
||||
};
|
||||
|
||||
@@ -32,7 +32,7 @@ export class ResponseBuilder {
|
||||
type: 'message',
|
||||
role: accumulator.getRole(),
|
||||
content: [],
|
||||
model: accumulator.getModel() || 'glm-4.6',
|
||||
model: accumulator.getModel() || 'glm-4.7',
|
||||
stop_reason: null,
|
||||
usage: {
|
||||
input_tokens: accumulator.getInputTokens(),
|
||||
|
||||
@@ -35,10 +35,10 @@ module.exports = {
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL: "https://api.z.ai/api/anthropic",
|
||||
ANTHROPIC_AUTH_TOKEN: "your_api_key_here",
|
||||
ANTHROPIC_MODEL: "glm-4.6",
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "glm-4.6",
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: "glm-4.6",
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: "glm-4.6"
|
||||
ANTHROPIC_MODEL: "glm-4.7",
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "glm-4.7",
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: "glm-4.7",
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: "glm-4.7"
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "ui",
|
||||
|
||||
@@ -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';
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Project Name Utility Functions
|
||||
*
|
||||
* Utility functions for extracting meaningful project names from file paths
|
||||
*/
|
||||
|
||||
/**
|
||||
* Extracts the leaf folder name from a project path
|
||||
*
|
||||
* This function takes a full project path and returns just the leaf folder name,
|
||||
* which represents the actual project name that the user would recognize.
|
||||
*
|
||||
* Examples:
|
||||
* - '/home/user/projects/my-app' → 'my-app'
|
||||
* - '/Users/joe/Developer/share-pi' → 'share-pi'
|
||||
* - '/Users/joe/Developer/ExaDev/.../worktrees/2026-01-08' → '2026-01-08'
|
||||
*
|
||||
* @param path - The full project path
|
||||
* @returns The leaf folder name (project name)
|
||||
*/
|
||||
export function getProjectDisplayName(path: string): string {
|
||||
if (!path) return '';
|
||||
|
||||
// Remove leading/trailing slashes and split into segments
|
||||
const cleanPath = path.replace(/^\/|\/$/g, '');
|
||||
const segments = cleanPath.split('/').filter((segment) => segment.length > 0);
|
||||
|
||||
// Return the last segment (leaf folder name)
|
||||
return segments[segments.length - 1] || '';
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import type { PaginatedSessions } from '@/hooks/use-usage';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
|
||||
import { getProjectDisplayName } from './project-name-utils';
|
||||
|
||||
interface SessionStatsCardProps {
|
||||
data: PaginatedSessions | undefined;
|
||||
@@ -131,7 +132,7 @@ export function SessionStatsCard({ data, isLoading, className }: SessionStatsCar
|
||||
>
|
||||
<div className="flex flex-col min-w-0 flex-1">
|
||||
<span className="font-medium truncate" title={session.projectPath}>
|
||||
{session.projectPath.split('/').pop()}
|
||||
{getProjectDisplayName(session.projectPath)}
|
||||
</span>
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{formatDistanceToNow(new Date(session.lastActivity), { addSuffix: true })}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Project Name Display Tests
|
||||
* Unit tests for getProjectDisplayName function
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
// Import the function from the utility
|
||||
import { getProjectDisplayName } from '../../../../src/components/analytics/project-name-utils';
|
||||
|
||||
describe('getProjectDisplayName', () => {
|
||||
describe('Simple project paths', () => {
|
||||
it('returns the leaf folder name for simple paths', () => {
|
||||
expect(getProjectDisplayName('/home/user/projects/my-app')).toBe('my-app');
|
||||
expect(getProjectDisplayName('/Users/joe/Developer/share-pi')).toBe('share-pi');
|
||||
expect(getProjectDisplayName('/var/www/html')).toBe('html');
|
||||
});
|
||||
|
||||
it('handles paths without leading/trailing slashes', () => {
|
||||
expect(getProjectDisplayName('home/user/projects/my-app')).toBe('my-app');
|
||||
expect(getProjectDisplayName('Users/joe/Developer/share-pi')).toBe('share-pi');
|
||||
});
|
||||
});
|
||||
|
||||
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('/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('/opt/apps/my-app/lib/utils')).toBe('utils');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('handles empty string', () => {
|
||||
expect(getProjectDisplayName('')).toBe('');
|
||||
});
|
||||
|
||||
it('handles only slashes', () => {
|
||||
expect(getProjectDisplayName('///')).toBe('');
|
||||
expect(getProjectDisplayName('/')).toBe('');
|
||||
});
|
||||
|
||||
it('handles single segment paths', () => {
|
||||
expect(getProjectDisplayName('my-app')).toBe('my-app');
|
||||
expect(getProjectDisplayName('project')).toBe('project');
|
||||
});
|
||||
|
||||
it('handles paths with trailing slash', () => {
|
||||
expect(getProjectDisplayName('/home/user/projects/my-app/')).toBe('my-app');
|
||||
expect(getProjectDisplayName('/Users/joe/Developer/share-pi/')).toBe('share-pi');
|
||||
});
|
||||
|
||||
it('handles paths with leading slash only', () => {
|
||||
expect(getProjectDisplayName('/my-app')).toBe('my-app');
|
||||
expect(getProjectDisplayName('/project')).toBe('project');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Real-world examples from the bug report', () => {
|
||||
it('displays correct project name for share-pi project', () => {
|
||||
// Before fix: would show "pi"
|
||||
// After fix: should show "share-pi"
|
||||
const path = '/Users/joe/Developer/share-pi';
|
||||
expect(getProjectDisplayName(path)).toBe('share-pi');
|
||||
});
|
||||
|
||||
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';
|
||||
expect(getProjectDisplayName(path)).toBe('2026-01-08');
|
||||
});
|
||||
|
||||
it('displays correct project name for nested project', () => {
|
||||
// Example: a project in a subdirectory
|
||||
const path = '/home/user/dev/company/projects/web-app';
|
||||
expect(getProjectDisplayName(path)).toBe('web-app');
|
||||
});
|
||||
|
||||
it('displays correct project name for repo with worktrees', () => {
|
||||
// Example: main repository
|
||||
const path = '/Users/joe/Developer/my-repo';
|
||||
expect(getProjectDisplayName(path)).toBe('my-repo');
|
||||
});
|
||||
|
||||
it('displays correct project name for feature branch worktree', () => {
|
||||
// Example: feature branch worktree
|
||||
const path = '/Users/joe/Developer/my-repo/.git/worktrees/feature-x';
|
||||
expect(getProjectDisplayName(path)).toBe('feature-x');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Regression tests', () => {
|
||||
it('does not return empty string for valid paths', () => {
|
||||
const testCases = ['/project', '/home/user/app', '/var/log/nginx', '/tmp/test-file'];
|
||||
|
||||
testCases.forEach((path) => {
|
||||
const result = getProjectDisplayName(path);
|
||||
expect(result).not.toBe('');
|
||||
expect(result).not.toBeUndefined();
|
||||
expect(result).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('handles paths with special characters', () => {
|
||||
expect(getProjectDisplayName('/home/user/my-project_v2')).toBe('my-project_v2');
|
||||
expect(getProjectDisplayName('/home/user/project-with-dashes')).toBe('project-with-dashes');
|
||||
expect(getProjectDisplayName('/home/user/project.with.dots')).toBe('project.with.dots');
|
||||
});
|
||||
|
||||
it('handles numeric paths correctly', () => {
|
||||
expect(getProjectDisplayName('/home/user/project123')).toBe('project123');
|
||||
expect(getProjectDisplayName('/home/user/123project')).toBe('123project');
|
||||
expect(getProjectDisplayName('/home/user/v1.2.3')).toBe('v1.2.3');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,305 @@
|
||||
/**
|
||||
* Session Stats Card Tests
|
||||
* Unit tests for SessionStatsCard component with project name formatting
|
||||
*/
|
||||
|
||||
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 { AllProviders } from '../../../setup/test-utils';
|
||||
import type { PaginatedSessions } from '../../../../src/hooks/use-usage';
|
||||
|
||||
// Mock date-fns to return consistent dates
|
||||
vi.mock('date-fns', async () => {
|
||||
const actual = await vi.importActual('date-fns');
|
||||
return {
|
||||
...actual,
|
||||
formatDistanceToNow: vi.fn(() => '27 minutes ago'),
|
||||
};
|
||||
});
|
||||
|
||||
describe('SessionStatsCard', () => {
|
||||
beforeEach(() => {
|
||||
// Reset all mocks
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Loading and Empty States', () => {
|
||||
it('renders loading skeleton when isLoading is true', () => {
|
||||
const { container } = render(<SessionStatsCard data={undefined} isLoading={true} />, {
|
||||
wrapper: AllProviders,
|
||||
});
|
||||
|
||||
// Should have skeleton loading elements
|
||||
expect(container.querySelector('[data-slot="skeleton"]')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows empty state when no data available', () => {
|
||||
render(<SessionStatsCard data={undefined} />, { wrapper: AllProviders });
|
||||
|
||||
expect(screen.getByText('Session Stats')).toBeInTheDocument();
|
||||
expect(screen.getByText('No session data available')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows empty state when sessions array is empty', () => {
|
||||
const emptyData: PaginatedSessions = {
|
||||
sessions: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
};
|
||||
|
||||
render(<SessionStatsCard data={emptyData} />, { wrapper: AllProviders });
|
||||
|
||||
expect(screen.getByText('Session Stats')).toBeInTheDocument();
|
||||
expect(screen.getByText('No session data available')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Session Stats Display', () => {
|
||||
const createMockSession = (
|
||||
projectPath: string,
|
||||
inputTokens: number,
|
||||
outputTokens: number,
|
||||
cost: number
|
||||
) => ({
|
||||
sessionId: `session-${Math.random()}`,
|
||||
projectPath,
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
cost,
|
||||
lastActivity: new Date().toISOString(),
|
||||
});
|
||||
|
||||
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('/Users/joe/Developer/share-pi', 1000, 2000, 0.05),
|
||||
],
|
||||
total: 3,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
};
|
||||
|
||||
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('/Users/joe/Developer/share-pi', 1000, 2000, 0.05),
|
||||
];
|
||||
});
|
||||
|
||||
it('displays session stats header', () => {
|
||||
render(<SessionStatsCard data={mockData} />, { wrapper: AllProviders });
|
||||
|
||||
expect(screen.getByText('Session Stats')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows total sessions count', () => {
|
||||
render(<SessionStatsCard data={mockData} />, { wrapper: AllProviders });
|
||||
|
||||
expect(screen.getByText('3')).toBeInTheDocument();
|
||||
expect(screen.getByText('Total Sessions')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calculates and displays average cost per session', () => {
|
||||
render(<SessionStatsCard data={mockData} />, { wrapper: AllProviders });
|
||||
|
||||
// Average cost: (0.08 + 0.12 + 0.05) / 3 = 0.0833 → $0.08
|
||||
// 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();
|
||||
});
|
||||
|
||||
it('shows recent activity section', () => {
|
||||
render(<SessionStatsCard data={mockData} />, { wrapper: AllProviders });
|
||||
|
||||
expect(screen.getByText('Recent Activity')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Project Name Formatting', () => {
|
||||
it('displays correct project name for simple path', () => {
|
||||
const mockData: PaginatedSessions = {
|
||||
sessions: [
|
||||
{
|
||||
sessionId: '1',
|
||||
projectPath: '/home/user/projects/my-app',
|
||||
inputTokens: 1000,
|
||||
outputTokens: 2000,
|
||||
cost: 0.05,
|
||||
lastActivity: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
};
|
||||
|
||||
render(<SessionStatsCard data={mockData} />, { wrapper: AllProviders });
|
||||
|
||||
// Should show "my-app" instead of just "app"
|
||||
expect(screen.getByTitle('/home/user/projects/my-app')).toHaveTextContent('my-app');
|
||||
});
|
||||
|
||||
it('displays correct project name for worktree path', () => {
|
||||
const mockData: PaginatedSessions = {
|
||||
sessions: [
|
||||
{
|
||||
sessionId: '1',
|
||||
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,
|
||||
pageSize: 10,
|
||||
};
|
||||
|
||||
render(<SessionStatsCard data={mockData} />, { 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');
|
||||
});
|
||||
|
||||
it('displays correct project name for shared project', () => {
|
||||
const mockData: PaginatedSessions = {
|
||||
sessions: [
|
||||
{
|
||||
sessionId: '1',
|
||||
projectPath: '/Users/joe/Developer/share-pi',
|
||||
inputTokens: 1000,
|
||||
outputTokens: 2000,
|
||||
cost: 0.05,
|
||||
lastActivity: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
};
|
||||
|
||||
render(<SessionStatsCard data={mockData} />, { wrapper: AllProviders });
|
||||
|
||||
// Should show "share-pi" instead of just "pi"
|
||||
expect(screen.getByTitle('/Users/joe/Developer/share-pi')).toHaveTextContent('share-pi');
|
||||
});
|
||||
|
||||
it('handles empty project path gracefully', () => {
|
||||
const mockData: PaginatedSessions = {
|
||||
sessions: [
|
||||
{
|
||||
sessionId: '1',
|
||||
projectPath: '',
|
||||
inputTokens: 1000,
|
||||
outputTokens: 2000,
|
||||
cost: 0.05,
|
||||
lastActivity: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
};
|
||||
|
||||
render(<SessionStatsCard data={mockData} />, { wrapper: AllProviders });
|
||||
|
||||
// Should not crash and show empty string
|
||||
expect(screen.getByTitle('')).toHaveTextContent('');
|
||||
});
|
||||
|
||||
it('handles project path with only slashes', () => {
|
||||
const mockData: PaginatedSessions = {
|
||||
sessions: [
|
||||
{
|
||||
sessionId: '1',
|
||||
projectPath: '///',
|
||||
inputTokens: 1000,
|
||||
outputTokens: 2000,
|
||||
cost: 0.05,
|
||||
lastActivity: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
};
|
||||
|
||||
render(<SessionStatsCard data={mockData} />, { wrapper: AllProviders });
|
||||
|
||||
// Should handle gracefully and show empty string
|
||||
expect(screen.getByTitle('///')).toHaveTextContent('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Token Count Display', () => {
|
||||
it('displays token counts in compact format', () => {
|
||||
const mockData: PaginatedSessions = {
|
||||
sessions: [
|
||||
{
|
||||
sessionId: '1',
|
||||
projectPath: '/project/test',
|
||||
inputTokens: 1500000, // 1.5M
|
||||
outputTokens: 500000, // 500K
|
||||
cost: 0.1,
|
||||
lastActivity: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
};
|
||||
|
||||
render(<SessionStatsCard data={mockData} />, { wrapper: AllProviders });
|
||||
|
||||
expect(screen.getByText('2.0M toks')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Privacy Mode', () => {
|
||||
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 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(<SessionStatsCard data={testData} />, { wrapper: AllProviders });
|
||||
|
||||
// Component should render without errors
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user