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:
Eyejoker
2026-03-25 12:32:41 +09:00
parent 18788f7f91
commit fc85fece0f
4 changed files with 428 additions and 393 deletions

View 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,
});
});
});