fix: remove Codex API key auth path, add secret redaction and d7 auto-rotation

- Remove OPENAI_API_KEY from .env and Codex child process env to prevent
  API billing when subscription quota is exhausted
- Remove writeCodexApiKeyAuth entirely — Codex now uses OAuth only
- Add 9-pattern secret redaction in formatOutbound() to prevent key leaks
- Fix Codex usage bucket aggregation: use 'codex' bucket only instead of
  max across all buckets (bengalfox = Codex Spark, not needed)
- Add d7≥100% auto-rotation in updateCodexAccountUsage to skip exhausted
  accounts and prevent API billing fallback
- Add findNextCodexAvailable that checks both rate-limits and d7 usage
- Clean stale apikey auth.json files from all session directories
- Update tests: OAuth-only auth, d7 auto-skip (3 new tests), dashboard

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eyejoker
2026-03-25 10:39:42 +09:00
parent 094278b08e
commit 00ffde7623
11 changed files with 818 additions and 192 deletions

View File

@@ -18,8 +18,82 @@ vi.mock('./codex-token-rotation.js', () => ({
import {
buildClaudeUsageRows,
extractCodexUsageRows,
mergeClaudeDashboardAccounts,
} from './unified-dashboard.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('unified dashboard Claude usage rows', () => {
it('keeps both Claude accounts visible when one account usage is unavailable', () => {