kimi-code/packages/node-sdk/test/kimi-code-model-provider.test.ts
7Sageer 843a731097
fix: classify OAuth token refresh errors by cause (#838)
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.
2026-06-17 18:34:42 +08:00

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);
});
});