feat: show live Codex usage status in dashboard
Store Codex wham/usage live plan and rate-limit state during account refresh, surface safe dashboard badges, and keep JWT subscription expiry as stale fallback only. Redacts balance and spend limit amounts from API/UI exposure.
This commit is contained in:
@@ -2,33 +2,63 @@ import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { getCodexFeatures, updateCodexFeatures } from './settings-store.js';
|
||||
import {
|
||||
getCodexFeatures,
|
||||
listCodexAccounts,
|
||||
refreshCodexAccount,
|
||||
updateCodexFeatures,
|
||||
} from './settings-store.js';
|
||||
|
||||
describe('settings-store Codex features', () => {
|
||||
let tempDir: string;
|
||||
let previousCwd: string;
|
||||
let previousCodexGoals: string | undefined;
|
||||
let previousHome: string | undefined;
|
||||
let previousSettingsHome: string | undefined;
|
||||
let previousFetch: typeof fetch;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-settings-'));
|
||||
previousCwd = process.cwd();
|
||||
previousCodexGoals = process.env.CODEX_GOALS;
|
||||
previousHome = process.env.HOME;
|
||||
previousSettingsHome = process.env.EJCLAW_SETTINGS_HOME;
|
||||
previousFetch = globalThis.fetch;
|
||||
delete process.env.CODEX_GOALS;
|
||||
process.env.HOME = tempDir;
|
||||
process.env.EJCLAW_SETTINGS_HOME = tempDir;
|
||||
process.chdir(tempDir);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.chdir(previousCwd);
|
||||
globalThis.fetch = previousFetch;
|
||||
if (previousCodexGoals === undefined) {
|
||||
delete process.env.CODEX_GOALS;
|
||||
} else {
|
||||
process.env.CODEX_GOALS = previousCodexGoals;
|
||||
}
|
||||
if (previousHome === undefined) {
|
||||
delete process.env.HOME;
|
||||
} else {
|
||||
process.env.HOME = previousHome;
|
||||
}
|
||||
if (previousSettingsHome === undefined) {
|
||||
delete process.env.EJCLAW_SETTINGS_HOME;
|
||||
} else {
|
||||
process.env.EJCLAW_SETTINGS_HOME = previousSettingsHome;
|
||||
}
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function fakeJwt(payload: Record<string, unknown>): string {
|
||||
const encode = (value: Record<string, unknown>) =>
|
||||
Buffer.from(JSON.stringify(value)).toString('base64');
|
||||
return `${encode({ alg: 'none', typ: 'JWT' })}.${encode(payload)}.`;
|
||||
}
|
||||
|
||||
it('stores the Codex goals opt-in in the EJClaw .env file', () => {
|
||||
expect(getCodexFeatures()).toEqual({ goals: false });
|
||||
|
||||
@@ -42,4 +72,123 @@ describe('settings-store Codex features', () => {
|
||||
'CODEX_GOALS=false',
|
||||
);
|
||||
});
|
||||
|
||||
it('stores wham usage status and does not expose stale JWT expiry as live billing', async () => {
|
||||
const accountDir = path.join(tempDir, '.codex-accounts', '1');
|
||||
fs.mkdirSync(accountDir, { recursive: true });
|
||||
const staleJwt = fakeJwt({
|
||||
email: 'cached@example.com',
|
||||
sub: 'acct_cached',
|
||||
'https://api.openai.com/auth': {
|
||||
chatgpt_plan_type: 'free',
|
||||
chatgpt_subscription_active_until: '2026-01-01T00:00:00.000Z',
|
||||
chatgpt_subscription_last_checked: '2026-01-01T00:00:00.000Z',
|
||||
},
|
||||
});
|
||||
fs.writeFileSync(
|
||||
path.join(accountDir, 'auth.json'),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
tokens: {
|
||||
access_token: 'old-access',
|
||||
id_token: staleJwt,
|
||||
refresh_token: 'refresh-token',
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
|
||||
const resetAt = 1_779_300_000;
|
||||
const fetchMock = vi.fn(async (input: string | URL | Request) => {
|
||||
const url = String(input);
|
||||
if (url === 'https://auth.openai.com/oauth/token') {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
access_token: 'live-access',
|
||||
id_token: staleJwt,
|
||||
refresh_token: 'next-refresh-token',
|
||||
}),
|
||||
{ status: 200 },
|
||||
);
|
||||
}
|
||||
if (url === 'https://chatgpt.com/backend-api/wham/usage') {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
email: 'live@example.com',
|
||||
plan_type: 'pro',
|
||||
rate_limit: {
|
||||
allowed: true,
|
||||
limit_reached: false,
|
||||
primary_window: {
|
||||
limit_window_seconds: 18000,
|
||||
reset_after_seconds: 600,
|
||||
reset_at: resetAt,
|
||||
used_percent: 21,
|
||||
},
|
||||
secondary_window: {
|
||||
limit_window_seconds: 604800,
|
||||
reset_after_seconds: 3600,
|
||||
reset_at: resetAt + 600,
|
||||
used_percent: 7,
|
||||
},
|
||||
},
|
||||
rate_limit_reached_type: null,
|
||||
credits: {
|
||||
has_credits: false,
|
||||
overage_limit_reached: false,
|
||||
unlimited: false,
|
||||
},
|
||||
spend_control: { reached: false },
|
||||
}),
|
||||
{ status: 200 },
|
||||
);
|
||||
}
|
||||
return new Response('not found', { status: 404 });
|
||||
});
|
||||
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
||||
|
||||
const refreshed = await refreshCodexAccount(1);
|
||||
|
||||
expect(refreshed.planType).toBe('pro');
|
||||
expect(refreshed.email).toBe('live@example.com');
|
||||
expect(refreshed.subscriptionUntil).toBeNull();
|
||||
expect(refreshed.subscriptionSource).toBeNull();
|
||||
expect(refreshed.liveStatus).toMatchObject({
|
||||
source: 'wham/usage',
|
||||
planType: 'pro',
|
||||
rateLimit: {
|
||||
allowed: true,
|
||||
limitReached: false,
|
||||
primaryWindow: {
|
||||
limitWindowSeconds: 18000,
|
||||
resetAfterSeconds: 600,
|
||||
resetAt: new Date(resetAt * 1000).toISOString(),
|
||||
usedPercent: 21,
|
||||
},
|
||||
},
|
||||
credits: {
|
||||
hasCredits: false,
|
||||
overageLimitReached: false,
|
||||
unlimited: false,
|
||||
},
|
||||
spendControl: { reached: false },
|
||||
});
|
||||
|
||||
const sidecar = JSON.parse(
|
||||
fs.readFileSync(path.join(accountDir, 'plan-status.json'), 'utf-8'),
|
||||
) as Record<string, unknown>;
|
||||
expect(sidecar).toMatchObject({
|
||||
plan_type: 'pro',
|
||||
rate_limit: {
|
||||
primary_window: { used_percent: 21 },
|
||||
},
|
||||
});
|
||||
expect(listCodexAccounts()[0]).toMatchObject({
|
||||
planType: 'pro',
|
||||
subscriptionUntil: null,
|
||||
liveStatus: { source: 'wham/usage' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user