mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
fix: enhance error handling and output for non-interactive authentication in JSON and STREAM_JSON modes
This commit is contained in:
@@ -16,10 +16,6 @@
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
},
|
||||
"./protocol": {
|
||||
"types": "./dist/src/types/protocol.d.ts",
|
||||
"import": "./dist/src/types/protocol.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -10,6 +10,9 @@ import { AuthType, OutputFormat } from '@qwen-code/qwen-code-core';
|
||||
import type { Config } from '@qwen-code/qwen-code-core';
|
||||
import * as auth from './config/auth.js';
|
||||
import { type LoadedSettings } from './config/settings.js';
|
||||
import * as JsonOutputAdapterModule from './nonInteractive/io/JsonOutputAdapter.js';
|
||||
import * as StreamJsonOutputAdapterModule from './nonInteractive/io/StreamJsonOutputAdapter.js';
|
||||
import * as cleanupModule from './utils/cleanup.js';
|
||||
|
||||
describe('validateNonInterActiveAuth', () => {
|
||||
let originalEnvGeminiApiKey: string | undefined;
|
||||
@@ -17,8 +20,8 @@ describe('validateNonInterActiveAuth', () => {
|
||||
let originalEnvGcp: string | undefined;
|
||||
let originalEnvOpenAiApiKey: string | undefined;
|
||||
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
|
||||
let processExitSpy: ReturnType<typeof vi.spyOn>;
|
||||
let refreshAuthMock: vi.Mock;
|
||||
let processExitSpy: ReturnType<typeof vi.spyOn<[code?: number], never>>;
|
||||
let refreshAuthMock: ReturnType<typeof vi.fn>;
|
||||
let mockSettings: LoadedSettings;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -33,7 +36,7 @@ describe('validateNonInterActiveAuth', () => {
|
||||
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
processExitSpy = vi.spyOn(process, 'exit').mockImplementation((code) => {
|
||||
throw new Error(`process.exit(${code}) called`);
|
||||
});
|
||||
}) as ReturnType<typeof vi.spyOn<[code?: number], never>>;
|
||||
refreshAuthMock = vi.fn().mockResolvedValue('refreshed');
|
||||
mockSettings = {
|
||||
system: { path: '', settings: {} },
|
||||
@@ -235,7 +238,24 @@ describe('validateNonInterActiveAuth', () => {
|
||||
});
|
||||
|
||||
describe('JSON output mode', () => {
|
||||
it('prints JSON error when no auth is configured and exits with code 1', async () => {
|
||||
let emitResultMock: ReturnType<typeof vi.fn>;
|
||||
let runExitCleanupMock: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
emitResultMock = vi.fn();
|
||||
runExitCleanupMock = vi.fn().mockResolvedValue(undefined);
|
||||
vi.spyOn(JsonOutputAdapterModule, 'JsonOutputAdapter').mockImplementation(
|
||||
() =>
|
||||
({
|
||||
emitResult: emitResultMock,
|
||||
}) as unknown as JsonOutputAdapterModule.JsonOutputAdapter,
|
||||
);
|
||||
vi.spyOn(cleanupModule, 'runExitCleanup').mockImplementation(
|
||||
runExitCleanupMock,
|
||||
);
|
||||
});
|
||||
|
||||
it('emits error result and exits when no auth is configured', async () => {
|
||||
const nonInteractiveConfig = {
|
||||
refreshAuth: refreshAuthMock,
|
||||
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.JSON),
|
||||
@@ -244,7 +264,6 @@ describe('validateNonInterActiveAuth', () => {
|
||||
.mockReturnValue({ authType: undefined }),
|
||||
} as unknown as Config;
|
||||
|
||||
let thrown: Error | undefined;
|
||||
try {
|
||||
await validateNonInteractiveAuth(
|
||||
undefined,
|
||||
@@ -252,21 +271,27 @@ describe('validateNonInterActiveAuth', () => {
|
||||
nonInteractiveConfig,
|
||||
mockSettings,
|
||||
);
|
||||
expect.fail('Should have exited');
|
||||
} catch (e) {
|
||||
thrown = e as Error;
|
||||
expect((e as Error).message).toContain('process.exit(1) called');
|
||||
}
|
||||
|
||||
expect(thrown?.message).toBe('process.exit(1) called');
|
||||
const errorArg = consoleErrorSpy.mock.calls[0]?.[0] as string;
|
||||
const payload = JSON.parse(errorArg);
|
||||
expect(payload.error.type).toBe('Error');
|
||||
expect(payload.error.code).toBe(1);
|
||||
expect(payload.error.message).toContain(
|
||||
'Please set an Auth method in your',
|
||||
);
|
||||
expect(emitResultMock).toHaveBeenCalledWith({
|
||||
isError: true,
|
||||
errorMessage: expect.stringContaining(
|
||||
'Please set an Auth method in your',
|
||||
),
|
||||
durationMs: 0,
|
||||
apiDurationMs: 0,
|
||||
numTurns: 0,
|
||||
usage: undefined,
|
||||
});
|
||||
expect(runExitCleanupMock).toHaveBeenCalled();
|
||||
expect(processExitSpy).toHaveBeenCalledWith(1);
|
||||
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('prints JSON error when enforced auth mismatches current auth and exits with code 1', async () => {
|
||||
it('emits error result and exits when enforced auth mismatches current auth', async () => {
|
||||
mockSettings.merged.security!.auth!.enforcedType = AuthType.QWEN_OAUTH;
|
||||
process.env['OPENAI_API_KEY'] = 'fake-key';
|
||||
|
||||
@@ -278,7 +303,6 @@ describe('validateNonInterActiveAuth', () => {
|
||||
.mockReturnValue({ authType: undefined }),
|
||||
} as unknown as Config;
|
||||
|
||||
let thrown: Error | undefined;
|
||||
try {
|
||||
await validateNonInteractiveAuth(
|
||||
undefined,
|
||||
@@ -286,23 +310,27 @@ describe('validateNonInterActiveAuth', () => {
|
||||
nonInteractiveConfig,
|
||||
mockSettings,
|
||||
);
|
||||
expect.fail('Should have exited');
|
||||
} catch (e) {
|
||||
thrown = e as Error;
|
||||
expect((e as Error).message).toContain('process.exit(1) called');
|
||||
}
|
||||
|
||||
expect(thrown?.message).toBe('process.exit(1) called');
|
||||
{
|
||||
const errorArg = consoleErrorSpy.mock.calls[0]?.[0] as string;
|
||||
const payload = JSON.parse(errorArg);
|
||||
expect(payload.error.type).toBe('Error');
|
||||
expect(payload.error.code).toBe(1);
|
||||
expect(payload.error.message).toContain(
|
||||
expect(emitResultMock).toHaveBeenCalledWith({
|
||||
isError: true,
|
||||
errorMessage: expect.stringContaining(
|
||||
'The configured auth type is qwen-oauth, but the current auth type is openai.',
|
||||
);
|
||||
}
|
||||
),
|
||||
durationMs: 0,
|
||||
apiDurationMs: 0,
|
||||
numTurns: 0,
|
||||
usage: undefined,
|
||||
});
|
||||
expect(runExitCleanupMock).toHaveBeenCalled();
|
||||
expect(processExitSpy).toHaveBeenCalledWith(1);
|
||||
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('prints JSON error when validateAuthMethod fails and exits with code 1', async () => {
|
||||
it('emits error result and exits when validateAuthMethod fails', async () => {
|
||||
vi.spyOn(auth, 'validateAuthMethod').mockReturnValue('Auth error!');
|
||||
process.env['OPENAI_API_KEY'] = 'fake-key';
|
||||
|
||||
@@ -314,7 +342,6 @@ describe('validateNonInterActiveAuth', () => {
|
||||
.mockReturnValue({ authType: undefined }),
|
||||
} as unknown as Config;
|
||||
|
||||
let thrown: Error | undefined;
|
||||
try {
|
||||
await validateNonInteractiveAuth(
|
||||
AuthType.USE_OPENAI,
|
||||
@@ -322,18 +349,159 @@ describe('validateNonInterActiveAuth', () => {
|
||||
nonInteractiveConfig,
|
||||
mockSettings,
|
||||
);
|
||||
expect.fail('Should have exited');
|
||||
} catch (e) {
|
||||
thrown = e as Error;
|
||||
expect((e as Error).message).toContain('process.exit(1) called');
|
||||
}
|
||||
|
||||
expect(thrown?.message).toBe('process.exit(1) called');
|
||||
{
|
||||
const errorArg = consoleErrorSpy.mock.calls[0]?.[0] as string;
|
||||
const payload = JSON.parse(errorArg);
|
||||
expect(payload.error.type).toBe('Error');
|
||||
expect(payload.error.code).toBe(1);
|
||||
expect(payload.error.message).toBe('Auth error!');
|
||||
expect(emitResultMock).toHaveBeenCalledWith({
|
||||
isError: true,
|
||||
errorMessage: 'Auth error!',
|
||||
durationMs: 0,
|
||||
apiDurationMs: 0,
|
||||
numTurns: 0,
|
||||
usage: undefined,
|
||||
});
|
||||
expect(runExitCleanupMock).toHaveBeenCalled();
|
||||
expect(processExitSpy).toHaveBeenCalledWith(1);
|
||||
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('STREAM_JSON output mode', () => {
|
||||
let emitResultMock: ReturnType<typeof vi.fn>;
|
||||
let runExitCleanupMock: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
emitResultMock = vi.fn();
|
||||
runExitCleanupMock = vi.fn().mockResolvedValue(undefined);
|
||||
vi.spyOn(
|
||||
StreamJsonOutputAdapterModule,
|
||||
'StreamJsonOutputAdapter',
|
||||
).mockImplementation(
|
||||
() =>
|
||||
({
|
||||
emitResult: emitResultMock,
|
||||
}) as unknown as StreamJsonOutputAdapterModule.StreamJsonOutputAdapter,
|
||||
);
|
||||
vi.spyOn(cleanupModule, 'runExitCleanup').mockImplementation(
|
||||
runExitCleanupMock,
|
||||
);
|
||||
});
|
||||
|
||||
it('emits error result and exits when no auth is configured', async () => {
|
||||
const nonInteractiveConfig = {
|
||||
refreshAuth: refreshAuthMock,
|
||||
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.STREAM_JSON),
|
||||
getIncludePartialMessages: vi.fn().mockReturnValue(false),
|
||||
getContentGeneratorConfig: vi
|
||||
.fn()
|
||||
.mockReturnValue({ authType: undefined }),
|
||||
} as unknown as Config;
|
||||
|
||||
try {
|
||||
await validateNonInteractiveAuth(
|
||||
undefined,
|
||||
undefined,
|
||||
nonInteractiveConfig,
|
||||
mockSettings,
|
||||
);
|
||||
expect.fail('Should have exited');
|
||||
} catch (e) {
|
||||
expect((e as Error).message).toContain('process.exit(1) called');
|
||||
}
|
||||
|
||||
expect(emitResultMock).toHaveBeenCalledWith({
|
||||
isError: true,
|
||||
errorMessage: expect.stringContaining(
|
||||
'Please set an Auth method in your',
|
||||
),
|
||||
durationMs: 0,
|
||||
apiDurationMs: 0,
|
||||
numTurns: 0,
|
||||
usage: undefined,
|
||||
});
|
||||
expect(runExitCleanupMock).toHaveBeenCalled();
|
||||
expect(processExitSpy).toHaveBeenCalledWith(1);
|
||||
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('emits error result and exits when enforced auth mismatches current auth', async () => {
|
||||
mockSettings.merged.security!.auth!.enforcedType = AuthType.QWEN_OAUTH;
|
||||
process.env['OPENAI_API_KEY'] = 'fake-key';
|
||||
|
||||
const nonInteractiveConfig = {
|
||||
refreshAuth: refreshAuthMock,
|
||||
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.STREAM_JSON),
|
||||
getIncludePartialMessages: vi.fn().mockReturnValue(false),
|
||||
getContentGeneratorConfig: vi
|
||||
.fn()
|
||||
.mockReturnValue({ authType: undefined }),
|
||||
} as unknown as Config;
|
||||
|
||||
try {
|
||||
await validateNonInteractiveAuth(
|
||||
undefined,
|
||||
undefined,
|
||||
nonInteractiveConfig,
|
||||
mockSettings,
|
||||
);
|
||||
expect.fail('Should have exited');
|
||||
} catch (e) {
|
||||
expect((e as Error).message).toContain('process.exit(1) called');
|
||||
}
|
||||
|
||||
expect(emitResultMock).toHaveBeenCalledWith({
|
||||
isError: true,
|
||||
errorMessage: expect.stringContaining(
|
||||
'The configured auth type is qwen-oauth, but the current auth type is openai.',
|
||||
),
|
||||
durationMs: 0,
|
||||
apiDurationMs: 0,
|
||||
numTurns: 0,
|
||||
usage: undefined,
|
||||
});
|
||||
expect(runExitCleanupMock).toHaveBeenCalled();
|
||||
expect(processExitSpy).toHaveBeenCalledWith(1);
|
||||
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('emits error result and exits when validateAuthMethod fails', async () => {
|
||||
vi.spyOn(auth, 'validateAuthMethod').mockReturnValue('Auth error!');
|
||||
process.env['OPENAI_API_KEY'] = 'fake-key';
|
||||
|
||||
const nonInteractiveConfig = {
|
||||
refreshAuth: refreshAuthMock,
|
||||
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.STREAM_JSON),
|
||||
getIncludePartialMessages: vi.fn().mockReturnValue(false),
|
||||
getContentGeneratorConfig: vi
|
||||
.fn()
|
||||
.mockReturnValue({ authType: undefined }),
|
||||
} as unknown as Config;
|
||||
|
||||
try {
|
||||
await validateNonInteractiveAuth(
|
||||
AuthType.USE_OPENAI,
|
||||
undefined,
|
||||
nonInteractiveConfig,
|
||||
mockSettings,
|
||||
);
|
||||
expect.fail('Should have exited');
|
||||
} catch (e) {
|
||||
expect((e as Error).message).toContain('process.exit(1) called');
|
||||
}
|
||||
|
||||
expect(emitResultMock).toHaveBeenCalledWith({
|
||||
isError: true,
|
||||
errorMessage: 'Auth error!',
|
||||
durationMs: 0,
|
||||
apiDurationMs: 0,
|
||||
numTurns: 0,
|
||||
usage: undefined,
|
||||
});
|
||||
expect(runExitCleanupMock).toHaveBeenCalled();
|
||||
expect(processExitSpy).toHaveBeenCalledWith(1);
|
||||
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,9 @@ import { AuthType, OutputFormat } from '@qwen-code/qwen-code-core';
|
||||
import { USER_SETTINGS_PATH } from './config/settings.js';
|
||||
import { validateAuthMethod } from './config/auth.js';
|
||||
import { type LoadedSettings } from './config/settings.js';
|
||||
import { handleError } from './utils/errors.js';
|
||||
import { JsonOutputAdapter } from './nonInteractive/io/JsonOutputAdapter.js';
|
||||
import { StreamJsonOutputAdapter } from './nonInteractive/io/StreamJsonOutputAdapter.js';
|
||||
import { runExitCleanup } from './utils/cleanup.js';
|
||||
|
||||
function getAuthTypeFromEnv(): AuthType | undefined {
|
||||
if (process.env['OPENAI_API_KEY']) {
|
||||
@@ -27,7 +29,7 @@ export async function validateNonInteractiveAuth(
|
||||
useExternalAuth: boolean | undefined,
|
||||
nonInteractiveConfig: Config,
|
||||
settings: LoadedSettings,
|
||||
) {
|
||||
): Promise<Config> {
|
||||
try {
|
||||
const enforcedType = settings.merged.security?.auth?.enforcedType;
|
||||
if (enforcedType) {
|
||||
@@ -58,15 +60,38 @@ export async function validateNonInteractiveAuth(
|
||||
await nonInteractiveConfig.refreshAuth(authType);
|
||||
return nonInteractiveConfig;
|
||||
} catch (error) {
|
||||
if (nonInteractiveConfig.getOutputFormat() === OutputFormat.JSON) {
|
||||
handleError(
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
nonInteractiveConfig,
|
||||
1,
|
||||
);
|
||||
} else {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
const outputFormat = nonInteractiveConfig.getOutputFormat();
|
||||
|
||||
// In JSON and STREAM_JSON modes, emit error result and exit
|
||||
if (
|
||||
outputFormat === OutputFormat.JSON ||
|
||||
outputFormat === OutputFormat.STREAM_JSON
|
||||
) {
|
||||
let adapter;
|
||||
if (outputFormat === OutputFormat.JSON) {
|
||||
adapter = new JsonOutputAdapter(nonInteractiveConfig);
|
||||
} else {
|
||||
adapter = new StreamJsonOutputAdapter(
|
||||
nonInteractiveConfig,
|
||||
nonInteractiveConfig.getIncludePartialMessages(),
|
||||
);
|
||||
}
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
adapter.emitResult({
|
||||
isError: true,
|
||||
errorMessage,
|
||||
durationMs: 0,
|
||||
apiDurationMs: 0,
|
||||
numTurns: 0,
|
||||
usage: undefined,
|
||||
});
|
||||
await runExitCleanup();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// For other modes (text), use existing error handling
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user