212 lines
5.6 KiB
TypeScript
212 lines
5.6 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
|
|
import type { ClaudeAccountUsage } from './claude-usage.js';
|
|
|
|
vi.mock('./claude-usage.js', () => ({
|
|
getClaudeProfile: (index: number) =>
|
|
index === 0
|
|
? { email: 'a@example.com', planType: 'max' }
|
|
: { email: 'b@example.com', planType: 'pro' },
|
|
}));
|
|
|
|
import {
|
|
buildClaudeUsageRows,
|
|
extractCodexUsageRows,
|
|
mergeClaudeDashboardAccounts,
|
|
} from './dashboard-usage-rows.js';
|
|
import type { StatusSnapshot } from './status-dashboard.js';
|
|
|
|
describe('extractCodexUsageRows staleness', () => {
|
|
const freshRows = [
|
|
{ name: 'Codex1* pro', h5pct: 42, h5reset: '2h', d7pct: 65, d7reset: '3d' },
|
|
];
|
|
const TEN_MIN = 600_000;
|
|
|
|
it('returns real rows when usageRowsFetchedAt is within maxAgeMs', () => {
|
|
const now = Date.now();
|
|
const snapshot: StatusSnapshot = {
|
|
serviceId: 'codex-main',
|
|
agentType: 'codex',
|
|
assistantName: 'test',
|
|
updatedAt: new Date(now).toISOString(),
|
|
entries: [],
|
|
usageRows: freshRows,
|
|
usageRowsFetchedAt: new Date(now - TEN_MIN + 1000).toISOString(), // 9min59s ago
|
|
};
|
|
|
|
const result = extractCodexUsageRows(snapshot, TEN_MIN, now);
|
|
expect(result).toEqual(freshRows);
|
|
});
|
|
|
|
it('returns degraded row when usageRowsFetchedAt exceeds maxAgeMs', () => {
|
|
const now = Date.now();
|
|
const snapshot: StatusSnapshot = {
|
|
serviceId: 'codex-main',
|
|
agentType: 'codex',
|
|
assistantName: 'test',
|
|
updatedAt: new Date(now).toISOString(), // heartbeat is fresh
|
|
entries: [],
|
|
usageRows: freshRows,
|
|
usageRowsFetchedAt: new Date(now - TEN_MIN - 1000).toISOString(), // 10min1s ago
|
|
};
|
|
|
|
const result = extractCodexUsageRows(snapshot, TEN_MIN, now);
|
|
expect(result).toEqual([
|
|
{ name: 'Codex', h5pct: -1, h5reset: '', d7pct: -1, d7reset: '' },
|
|
]);
|
|
});
|
|
|
|
it('returns degraded row when usageRowsFetchedAt is missing', () => {
|
|
const now = Date.now();
|
|
const snapshot: StatusSnapshot = {
|
|
serviceId: 'codex-main',
|
|
agentType: 'codex',
|
|
assistantName: 'test',
|
|
updatedAt: new Date(now).toISOString(),
|
|
entries: [],
|
|
usageRows: freshRows,
|
|
// usageRowsFetchedAt intentionally omitted
|
|
};
|
|
|
|
const result = extractCodexUsageRows(snapshot, TEN_MIN, now);
|
|
expect(result).toEqual([
|
|
{ name: 'Codex', h5pct: -1, h5reset: '', d7pct: -1, d7reset: '' },
|
|
]);
|
|
});
|
|
|
|
it('returns empty array when snapshot is undefined', () => {
|
|
expect(extractCodexUsageRows(undefined, TEN_MIN)).toEqual([]);
|
|
});
|
|
|
|
it('returns empty array when usageRows is empty', () => {
|
|
const snapshot: StatusSnapshot = {
|
|
serviceId: 'codex-main',
|
|
agentType: 'codex',
|
|
assistantName: 'test',
|
|
updatedAt: new Date().toISOString(),
|
|
entries: [],
|
|
usageRows: [],
|
|
usageRowsFetchedAt: new Date().toISOString(),
|
|
};
|
|
expect(extractCodexUsageRows(snapshot, TEN_MIN)).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('dashboard Claude usage rows', () => {
|
|
it('keeps both Claude accounts visible when one account usage is unavailable', () => {
|
|
const rows = buildClaudeUsageRows([
|
|
{
|
|
index: 0,
|
|
masked: 'tok-a',
|
|
isActive: true,
|
|
isRateLimited: false,
|
|
usage: {
|
|
five_hour: {
|
|
utilization: 0.4,
|
|
resets_at: '2026-03-24T04:00:00+09:00',
|
|
},
|
|
seven_day: {
|
|
utilization: 0.7,
|
|
resets_at: '2026-03-29T04:00:00+09:00',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
index: 1,
|
|
masked: 'tok-b',
|
|
isActive: false,
|
|
isRateLimited: true,
|
|
usage: null,
|
|
},
|
|
]);
|
|
|
|
expect(rows).toHaveLength(2);
|
|
expect(rows[0]).toMatchObject({
|
|
name: 'Claude1* max',
|
|
h5pct: 40,
|
|
d7pct: 70,
|
|
});
|
|
expect(rows[1]).toMatchObject({
|
|
name: 'Claude2! pro',
|
|
h5pct: -1,
|
|
d7pct: -1,
|
|
});
|
|
});
|
|
|
|
it('preserves the last successful usage per account instead of collapsing to one cache entry', () => {
|
|
const cachedAccounts: ClaudeAccountUsage[] = [
|
|
{
|
|
index: 0,
|
|
masked: 'tok-a',
|
|
isActive: false,
|
|
isRateLimited: false,
|
|
usage: {
|
|
five_hour: {
|
|
utilization: 0.25,
|
|
resets_at: '2026-03-24T04:00:00+09:00',
|
|
},
|
|
seven_day: {
|
|
utilization: 0.5,
|
|
resets_at: '2026-03-29T04:00:00+09:00',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
index: 1,
|
|
masked: 'tok-b',
|
|
isActive: true,
|
|
isRateLimited: false,
|
|
usage: {
|
|
five_hour: {
|
|
utilization: 0.6,
|
|
resets_at: '2026-03-24T06:00:00+09:00',
|
|
},
|
|
seven_day: {
|
|
utilization: 0.8,
|
|
resets_at: '2026-03-30T04:00:00+09:00',
|
|
},
|
|
},
|
|
},
|
|
];
|
|
const liveAccounts: ClaudeAccountUsage[] = [
|
|
{
|
|
index: 0,
|
|
masked: 'tok-a',
|
|
isActive: true,
|
|
isRateLimited: false,
|
|
usage: null,
|
|
},
|
|
{
|
|
index: 1,
|
|
masked: 'tok-b',
|
|
isActive: false,
|
|
isRateLimited: true,
|
|
usage: {
|
|
five_hour: {
|
|
utilization: 0.9,
|
|
resets_at: '2026-03-24T08:00:00+09:00',
|
|
},
|
|
seven_day: {
|
|
utilization: 0.95,
|
|
resets_at: '2026-03-31T04:00:00+09:00',
|
|
},
|
|
},
|
|
},
|
|
];
|
|
|
|
const merged = mergeClaudeDashboardAccounts(liveAccounts, cachedAccounts);
|
|
|
|
expect(merged).toHaveLength(2);
|
|
expect(merged[0]).toMatchObject({
|
|
index: 0,
|
|
isActive: true,
|
|
usage: cachedAccounts[0].usage,
|
|
});
|
|
expect(merged[1]).toMatchObject({
|
|
index: 1,
|
|
isRateLimited: true,
|
|
usage: liveAccounts[1].usage,
|
|
});
|
|
});
|
|
});
|