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

@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { EventEmitter } from 'events';
import fs from 'fs';
import { GroupQueue } from './group-queue.js';
import { GroupQueue, type GroupRunContext } from './group-queue.js';
// Mock config to control concurrency limit
vi.mock('./config.js', () => ({
@@ -121,6 +121,100 @@ describe('GroupQueue', () => {
await vi.advanceTimersByTimeAsync(10);
});
it('force-terminates a lingering process after output was delivered', async () => {
class StubbornProcess extends EventEmitter {
exitCode: number | null = null;
signalCode: NodeJS.Signals | null = null;
kill = vi.fn(() => true);
}
const ipcDir = '/tmp/ejclaw-test-data/ipc/group-folder';
let releaseRun!: (value: boolean) => void;
let runId: string | undefined;
const blocker = new Promise<boolean>((resolve) => {
releaseRun = resolve;
});
const processMessages = vi.fn(
async (_groupJid: string, context: GroupRunContext) => {
runId = context.runId;
return await blocker;
},
);
queue.setProcessMessagesFn(processMessages);
queue.enqueueMessageCheck('group1@g.us', ipcDir);
await vi.advanceTimersByTimeAsync(10);
expect(runId).toEqual(expect.any(String));
const proc = new StubbornProcess();
queue.registerProcess(
'group1@g.us',
proc as unknown as import('child_process').ChildProcess,
'proc-1',
ipcDir,
);
queue.closeStdin('group1@g.us', {
runId,
reason: 'output-delivered-close',
});
await vi.advanceTimersByTimeAsync(60_000);
expect(proc.kill).toHaveBeenCalledWith('SIGTERM');
await vi.advanceTimersByTimeAsync(15_000);
expect(proc.kill).toHaveBeenCalledWith('SIGKILL');
releaseRun(true);
await vi.advanceTimersByTimeAsync(10);
});
it('clears post-close termination timers once the run exits', async () => {
class FakeProcess extends EventEmitter {
exitCode: number | null = null;
signalCode: NodeJS.Signals | null = null;
kill = vi.fn(() => true);
}
const ipcDir = '/tmp/ejclaw-test-data/ipc/group-folder';
let releaseRun!: (value: boolean) => void;
const blocker = new Promise<boolean>((resolve) => {
releaseRun = resolve;
});
const proc = new FakeProcess();
const processMessages = vi.fn(
async (_groupJid: string, context: GroupRunContext) => {
queue.registerProcess(
'group1@g.us',
proc as unknown as import('child_process').ChildProcess,
'proc-1',
ipcDir,
);
queue.closeStdin('group1@g.us', {
runId: context.runId,
reason: 'output-delivered-close',
});
await blocker;
return true;
},
);
queue.setProcessMessagesFn(processMessages);
queue.enqueueMessageCheck('group1@g.us', ipcDir);
await vi.advanceTimersByTimeAsync(10);
releaseRun(true);
await vi.advanceTimersByTimeAsync(10);
await vi.advanceTimersByTimeAsync(75_000);
expect(proc.kill).not.toHaveBeenCalled();
});
// --- Global concurrency limit ---
it('respects global concurrency limit', async () => {