mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-11 09:47:40 +00:00
Map OAuth token-fetch failures to distinct public error codes instead of collapsing them all to auth.login_required: - missing/revoked tokens or 401/403 from the refresh endpoint -> auth.login_required - transport failures and 429/5xx after internal retries -> provider.connection_error - anything else is rethrown as-is (surfaces as internal) rather than guessed The refresh helper already retries internally, so the agent loop does not re-retry these. Both the managed provider and the standalone SDK provider share the same mapping.
76 lines
2.5 KiB
TypeScript
76 lines
2.5 KiB
TypeScript
import { mkdtemp, rm } from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
|
|
import {
|
|
KimiOAuthToolkit,
|
|
OAuthConnectionError,
|
|
OAuthError,
|
|
OAuthUnauthorizedError,
|
|
RetryableRefreshError,
|
|
} from '@moonshot-ai/kimi-code-oauth';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { ErrorCodes, KimiError, KimiForCodingProvider } from '#/index';
|
|
|
|
import { TEST_IDENTITY } from './test-identity';
|
|
|
|
describe('KimiForCodingProvider OAuth error mapping', () => {
|
|
let homeDir: string;
|
|
|
|
beforeEach(async () => {
|
|
homeDir = await mkdtemp(join(tmpdir(), 'kimi-for-coding-provider-'));
|
|
});
|
|
|
|
afterEach(async () => {
|
|
vi.restoreAllMocks();
|
|
await rm(homeDir, { recursive: true, force: true });
|
|
});
|
|
|
|
function resolveAuth() {
|
|
const provider = new KimiForCodingProvider({ homeDir, ...TEST_IDENTITY });
|
|
return provider.resolveAuth('kimi-for-coding');
|
|
}
|
|
|
|
it('maps unauthorized token failures to auth.login_required', async () => {
|
|
vi.spyOn(KimiOAuthToolkit.prototype, 'ensureFresh').mockRejectedValue(
|
|
new OAuthUnauthorizedError('No token for "kimi-code". Run /login to authenticate.'),
|
|
);
|
|
|
|
const auth = resolveAuth();
|
|
await expect(auth(async () => 'ok')).rejects.toMatchObject({
|
|
code: ErrorCodes.AUTH_LOGIN_REQUIRED,
|
|
});
|
|
});
|
|
|
|
it('maps transient token failures to provider.connection_error', async () => {
|
|
const tokenErrors = [
|
|
new OAuthConnectionError('OAuth request to https://example.test failed: fetch failed'),
|
|
new RetryableRefreshError('Token refresh failed (HTTP 503).'),
|
|
];
|
|
|
|
for (const tokenError of tokenErrors) {
|
|
vi.spyOn(KimiOAuthToolkit.prototype, 'ensureFresh').mockRejectedValue(tokenError);
|
|
|
|
const auth = resolveAuth();
|
|
const caught = await auth(async () => 'ok').catch((error: unknown) => error);
|
|
|
|
expect(caught).toBeInstanceOf(KimiError);
|
|
expect(caught).toMatchObject({
|
|
code: ErrorCodes.PROVIDER_CONNECTION_ERROR,
|
|
message: expect.stringContaining(tokenError.message),
|
|
cause: tokenError,
|
|
});
|
|
|
|
vi.restoreAllMocks();
|
|
}
|
|
});
|
|
|
|
it('rethrows unrecognized OAuth errors raw instead of guessing a category', async () => {
|
|
const oauthError = new OAuthError('Token refresh failed (HTTP 400).');
|
|
vi.spyOn(KimiOAuthToolkit.prototype, 'ensureFresh').mockRejectedValue(oauthError);
|
|
|
|
const auth = resolveAuth();
|
|
await expect(auth(async () => 'ok')).rejects.toBe(oauthError);
|
|
});
|
|
});
|