Files
EJClaw/src/agent-error-detection.test.ts
ejclaw 03d4c81192 fix: recover Codex rotation auth failures
- mark Codex bearer/refresh failures as terminal auth-expired states

- sync refreshed session auth back to rotation slots and revive refreshed dead_auth slots

- stop paired arbiter retry loops when Codex accounts are unavailable

- add regression coverage for rotation leases, follow-up suppression, and arbiter closure
2026-06-02 00:21:53 +09:00

105 lines
3.1 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
classifyAgentError,
classifyClaudeAuthError,
classifyCodexAuthError,
detectClaudeProviderFailureMessage,
isClaudeOrgAccessDeniedMessage,
shouldRotateClaudeToken,
} from './agent-error-detection.js';
describe('agent-error-detection', () => {
it('detects Claude org access denied banners', () => {
expect(
isClaudeOrgAccessDeniedMessage(
'Your organization does not have access to Claude. Please login again or contact your administrator.',
),
).toBe(true);
});
it('classifies org access denied banners as org-access-denied', () => {
expect(
classifyClaudeAuthError(
'Your organization does not have access to Claude. Please login again or contact your administrator.',
),
).toEqual({
category: 'org-access-denied',
reason: 'org-access-denied',
});
});
it('classifies terminated 403 auth failures as org-access-denied', () => {
expect(
classifyClaudeAuthError(
'Failed to authenticate. API Error: 403 terminated',
),
).toEqual({
category: 'org-access-denied',
reason: 'org-access-denied',
});
});
it('classifies Cloudflare 502 HTML as overloaded', () => {
const message = `API Error: 502 <html>
<head><title>502 Bad Gateway</title></head>
<body>
<center><h1>502 Bad Gateway</h1></center>
<hr><center>cloudflare</center>
</body>
</html>`;
expect(classifyAgentError(message)).toEqual({
category: 'overloaded',
reason: 'overloaded',
});
expect(detectClaudeProviderFailureMessage(message)).toBe('overloaded');
});
it('classifies Codex model capacity errors as overloaded', () => {
expect(
classifyAgentError(
'Selected model is at capacity. Please try a different model.',
),
).toEqual({
category: 'overloaded',
reason: 'overloaded',
});
});
it('classifies Codex reused refresh-token errors as auth-expired', () => {
expect(
classifyCodexAuthError(
'Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.',
),
).toEqual({
category: 'auth-expired',
reason: 'auth-expired',
});
});
it('classifies a Codex auth-expired trigger reason as auth-expired', () => {
expect(classifyCodexAuthError('auth-expired')).toEqual({
category: 'auth-expired',
reason: 'auth-expired',
});
});
it('classifies Codex workspace credit exhaustion as rate-limit', () => {
expect(classifyAgentError('Workspace out of credits')).toEqual({
category: 'rate-limit',
reason: '429',
retryAfterMs: undefined,
});
});
it('marks only Claude quota/auth reasons as Claude rotation reasons', () => {
expect(shouldRotateClaudeToken('429')).toBe(true);
expect(shouldRotateClaudeToken('usage-exhausted')).toBe(true);
expect(shouldRotateClaudeToken('auth-expired')).toBe(true);
expect(shouldRotateClaudeToken('org-access-denied')).toBe(true);
expect(shouldRotateClaudeToken('overloaded')).toBe(false);
expect(shouldRotateClaudeToken('success-null-result')).toBe(false);
});
});