mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-19 14:15:27 +00:00
Merge remote-tracking branch 'origin/dev' into codex/fix-logs-filtering-to-show-dashboard-audits
This commit is contained in:
commit
3d9636351e
156 files changed
+4169
-625
No files matched your search
@@ -94,6 +94,7 @@ describe('ControlPanelEmbed', () => {
|
||||
const iframe = await screen.findByTitle('CLIProxy Management Panel');
|
||||
|
||||
expect(iframe).toHaveAttribute('src', '/api/cliproxy-local/management.html');
|
||||
fireEvent.load(iframe);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.localStorage.removeItem).toHaveBeenCalledWith('cli-proxy-auth');
|
||||
@@ -107,6 +108,7 @@ describe('ControlPanelEmbed', () => {
|
||||
);
|
||||
expect(window.localStorage.setItem).toHaveBeenCalledWith('managementKey', 'custom-secret');
|
||||
expect(window.localStorage.setItem).toHaveBeenCalledWith('isLoggedIn', 'true');
|
||||
expect(window.localStorage.removeItem).toHaveBeenCalledWith('managementKey');
|
||||
});
|
||||
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -86,6 +86,47 @@ describe('useCliproxyAuthFlow', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('includes the selected account id when reauth starts from the dashboard', async () => {
|
||||
const fetchMock = vi.fn((input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
|
||||
if (url.includes('/codex/start-url')) {
|
||||
return Promise.resolve(
|
||||
createJsonResponse({
|
||||
success: true,
|
||||
authUrl: 'https://auth.example.com/codex?state=codex-reauth',
|
||||
state: 'codex-reauth',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (url.includes('/status?state=codex-reauth')) {
|
||||
return Promise.resolve(createJsonResponse({ status: 'wait' }));
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(`Unexpected fetch: ${url}`));
|
||||
});
|
||||
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
const { result } = renderHook(() => useCliproxyAuthFlow(), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
await result.current.startAuth('codex', {
|
||||
accountId: 'existing@example.com',
|
||||
startEndpoint: 'start-url',
|
||||
});
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'/api/cliproxy/auth/codex/start-url',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: expect.stringContaining('"accountId":"existing@example.com"'),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('surfaces manual OAuth start guidance from the dashboard API', async () => {
|
||||
const guidance = [
|
||||
'Start local CLIProxy first: ccs cliproxy start',
|
||||
|
||||
@@ -141,6 +141,65 @@ describe('deriveTraceGroups', () => {
|
||||
expect(result.every((r) => r.kind === 'leaf' && r.repeatCount === undefined)).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps adjacent leaves with different detail payloads as separate rows', () => {
|
||||
const result = deriveTraceGroups(
|
||||
leafEntries(
|
||||
{
|
||||
id: '1',
|
||||
timestamp: 't1',
|
||||
event: 'poll',
|
||||
message: 'same',
|
||||
latencyMs: 25,
|
||||
metadata: { attempt: 1, nested: { status: 'warm' } },
|
||||
context: { account: 'alpha' },
|
||||
error: { code: 'E_ONE', message: 'first' },
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
timestamp: 't2',
|
||||
event: 'poll',
|
||||
message: 'same',
|
||||
latencyMs: 50,
|
||||
metadata: { attempt: 2, nested: { status: 'cold' } },
|
||||
context: { account: 'beta' },
|
||||
error: { code: 'E_TWO', message: 'second' },
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result.every((r) => r.kind === 'leaf' && r.repeatCount === undefined)).toBe(true);
|
||||
expect(result.map((r) => (r.kind === 'leaf' ? r.entry.id : 'trace'))).toEqual(['2', '1']);
|
||||
});
|
||||
|
||||
it('coalesces leaves with semantically identical structured payloads', () => {
|
||||
const result = deriveTraceGroups(
|
||||
leafEntries(
|
||||
{
|
||||
id: '1',
|
||||
timestamp: 't1',
|
||||
event: 'poll',
|
||||
message: 'tick',
|
||||
latencyMs: 25,
|
||||
metadata: { b: 2, a: 1 },
|
||||
context: { nested: { b: false, a: true } },
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
timestamp: 't2',
|
||||
event: 'poll',
|
||||
message: 'tick',
|
||||
latencyMs: 25,
|
||||
metadata: { a: 1, b: 2 },
|
||||
context: { nested: { a: true, b: false } },
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect((result[0] as LeafItem).repeatCount).toBe(2);
|
||||
});
|
||||
|
||||
it('display-sorts items reverse-chronologically', () => {
|
||||
// Use distinct events so leaves don't coalesce — testing display sort,
|
||||
// not coalesce.
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { render, screen } from '@tests/setup/test-utils';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { LogsDetailPanel } from '@/components/logs/logs-detail-panel';
|
||||
import type { LogsEntry } from '@/lib/api-client';
|
||||
|
||||
function buildEntry(overrides: Partial<LogsEntry> = {}): LogsEntry {
|
||||
return {
|
||||
id: 'entry-1',
|
||||
timestamp: '2026-04-07T11:00:00.000Z',
|
||||
level: 'info',
|
||||
source: 'dashboard',
|
||||
event: 'logs.bootstrap',
|
||||
message: 'Dashboard log entry',
|
||||
processId: 25582,
|
||||
runId: 'run-1',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('LogsDetailPanel', () => {
|
||||
it('falls back to the raw timestamp when the selected log entry has an invalid timestamp', () => {
|
||||
render(<LogsDetailPanel entry={buildEntry({ timestamp: 'not-a-date' })} />);
|
||||
|
||||
expect(screen.getByText('not-a-date')).toBeInTheDocument();
|
||||
expect(screen.getByTitle('not-a-date')).toHaveTextContent('not-a-date');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,98 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { useAuthMonitorData } from '@/components/monitoring/auth-monitor/hooks';
|
||||
|
||||
const { useCliproxyAuthMock, useCliproxyStatsMock, useCliproxyStatusMock } = vi.hoisted(() => ({
|
||||
useCliproxyAuthMock: vi.fn(),
|
||||
useCliproxyStatsMock: vi.fn(),
|
||||
useCliproxyStatusMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/hooks/use-cliproxy', () => ({
|
||||
useCliproxyAuth: useCliproxyAuthMock,
|
||||
}));
|
||||
|
||||
vi.mock('@/hooks/use-cliproxy-stats', () => ({
|
||||
useCliproxyStats: useCliproxyStatsMock,
|
||||
useCliproxyStatus: useCliproxyStatusMock,
|
||||
}));
|
||||
|
||||
const authStatus = [
|
||||
{
|
||||
provider: 'codex',
|
||||
displayName: 'OpenAI Codex',
|
||||
accounts: [
|
||||
{
|
||||
id: 'codex-account',
|
||||
email: 'codex@example.com',
|
||||
tokenFile: '/tmp/codex.json',
|
||||
provider: 'codex',
|
||||
isDefault: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
describe('useAuthMonitorData', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
useCliproxyAuthMock.mockReturnValue({
|
||||
data: { authStatus },
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
useCliproxyStatsMock.mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
dataUpdatedAt: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps account data visible without polling stats when CLIProxy is unavailable', () => {
|
||||
useCliproxyStatusMock.mockReturnValue({
|
||||
data: { running: false },
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useAuthMonitorData());
|
||||
|
||||
expect(useCliproxyStatsMock).toHaveBeenCalledWith(false);
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(result.current.accounts).toHaveLength(1);
|
||||
expect(result.current.totalRequests).toBe(0);
|
||||
expect(result.current.providerStats[0]).toMatchObject({
|
||||
provider: 'codex',
|
||||
accountCount: 1,
|
||||
totalRequests: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('enables live stats only after CLIProxy is running', () => {
|
||||
useCliproxyStatusMock.mockReturnValue({
|
||||
data: { running: true },
|
||||
isLoading: false,
|
||||
});
|
||||
useCliproxyStatsMock.mockReturnValue({
|
||||
data: {
|
||||
accountStats: {
|
||||
'codex:codex@example.com': {
|
||||
source: 'codex@example.com',
|
||||
successCount: 8,
|
||||
failureCount: 2,
|
||||
totalTokens: 100,
|
||||
provider: 'codex',
|
||||
},
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
dataUpdatedAt: Date.now(),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useAuthMonitorData());
|
||||
|
||||
expect(useCliproxyStatsMock).toHaveBeenCalledWith(true);
|
||||
expect(result.current.totalSuccess).toBe(8);
|
||||
expect(result.current.totalFailure).toBe(2);
|
||||
expect(result.current.totalRequests).toBe(10);
|
||||
});
|
||||
});
|
||||
@@ -191,4 +191,41 @@ describe('AuthMonitor', () => {
|
||||
expect(within(getSummaryCard('Failed')).getByText('3')).toBeInTheDocument();
|
||||
expect(within(getSummaryCard('Success Rate')).getByText('80%')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows CLIProxy guidance when a large account pool has a high failure rate', () => {
|
||||
useAuthMonitorDataMock.mockReturnValue({
|
||||
...authMonitorData,
|
||||
accounts: Array.from({ length: 5 }, (_, index) => ({
|
||||
id: `codex-${index}`,
|
||||
email: `codex-${index}@example.com`,
|
||||
tokenFile: `/tmp/codex-${index}.json`,
|
||||
provider: 'codex',
|
||||
displayName: 'OpenAI Codex',
|
||||
isDefault: index === 0,
|
||||
successCount: index === 0 ? 1 : 0,
|
||||
failureCount: 5,
|
||||
color: '#10a37f',
|
||||
})),
|
||||
totalSuccess: 1,
|
||||
totalFailure: 25,
|
||||
totalRequests: 26,
|
||||
providerStats: [
|
||||
{
|
||||
provider: 'codex',
|
||||
displayName: 'OpenAI Codex',
|
||||
totalRequests: 26,
|
||||
successCount: 1,
|
||||
failureCount: 25,
|
||||
accountCount: 5,
|
||||
accounts: [],
|
||||
},
|
||||
],
|
||||
overallSuccessRate: 4,
|
||||
});
|
||||
|
||||
render(<AuthMonitor />);
|
||||
|
||||
expect(screen.getByText('High CLIProxy failure rate detected')).toBeInTheDocument();
|
||||
expect(screen.getByText(/ccs cliproxy routing/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -83,6 +83,20 @@ describe('QuotaTooltipContent', () => {
|
||||
expect(screen.getByText(expectedReset)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the Gemini tier label when entitlement evidence is absent', () => {
|
||||
const quota = createGeminiQuotaResult({
|
||||
entitlement: undefined,
|
||||
tierLabel: 'Legacy Pro',
|
||||
tierId: null,
|
||||
creditBalance: null,
|
||||
});
|
||||
|
||||
render(<QuotaTooltipContent quota={quota} resetTime={null} />);
|
||||
|
||||
expect(screen.getByText('Tier')).toBeInTheDocument();
|
||||
expect(screen.getByText('Legacy Pro')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('falls back to the shared reset indicator when Gemini buckets omit reset timestamps', () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-01-29T00:00:00Z'));
|
||||
|
||||
@@ -68,6 +68,7 @@ describe('provider presentation metadata', () => {
|
||||
'gitlab',
|
||||
'codebuddy',
|
||||
'kilo',
|
||||
'qoder',
|
||||
]);
|
||||
expect(getProviderSection('gitlab')?.id).toBe('plus-extra');
|
||||
expect(getProviderSection('gemini')?.id).toBe('core');
|
||||
@@ -128,6 +129,7 @@ describe('provider presentation metadata', () => {
|
||||
'/assets/providers/codebuddy.png',
|
||||
],
|
||||
['kilo', 'Kilo AI', 'Kilo AI coding assistant', '/assets/providers/kilo.png'],
|
||||
['qoder', 'Qoder', 'Qoder AI coding assistant', '/assets/providers/qoder.svg'],
|
||||
])('recognizes %s across dashboard display helpers', (provider, name, description, asset) => {
|
||||
expect(getProviderDisplayName(provider)).toBe(name);
|
||||
expect(getProviderDescription(provider)).toBe(description);
|
||||
|
||||
Reference in new issue
Block a user