refactor: split unified-dashboard into usage-rows and codex-collector modules
Extract dashboard-usage-rows.ts (UsageRow, formatResetRemaining, mergeClaudeDashboardAccounts, buildClaudeUsageRows, extractCodexUsageRows) and codex-usage-collector.ts (fetchCodexUsage, applyCodexUsageToAccount, buildCodexUsageRowsFromState, refresh functions returning data instead of mutating module state). unified-dashboard.ts drops from 926 to 563 lines. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
207
src/dashboard-usage-rows.test.ts
Normal file
207
src/dashboard-usage-rows.test.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
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 = {
|
||||
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 = {
|
||||
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 = {
|
||||
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 = {
|
||||
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,
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user