mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
refactor: update test structure and clean up unused code in cli and sdk
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
export { query } from './query/createQuery.js';
|
||||
|
||||
export { AbortError, isAbortError } from './types/errors.js';
|
||||
export { Query } from './query/Query.js';
|
||||
|
||||
export type { ExternalMcpServerConfig } from './types/queryOptionsSchema.js';
|
||||
|
||||
@@ -21,28 +21,20 @@ export function query({
|
||||
prompt: string | AsyncIterable<CLIUserMessage>;
|
||||
options?: QueryOptions;
|
||||
}): Query {
|
||||
// Validate options and obtain normalized executable metadata
|
||||
const parsedExecutable = validateOptions(options);
|
||||
|
||||
// Determine if this is a single-turn or multi-turn query
|
||||
// Single-turn: string prompt (simple Q&A)
|
||||
// Multi-turn: AsyncIterable prompt (streaming conversation)
|
||||
const isSingleTurn = typeof prompt === 'string';
|
||||
|
||||
// Resolve CLI specification while preserving explicit runtime directives
|
||||
const pathToQwenExecutable =
|
||||
options.pathToQwenExecutable ?? parsedExecutable.executablePath;
|
||||
|
||||
// Use provided abortController or create a new one
|
||||
const abortController = options.abortController ?? new AbortController();
|
||||
|
||||
// Create transport with abortController
|
||||
const transport = new ProcessTransport({
|
||||
pathToQwenExecutable,
|
||||
cwd: options.cwd,
|
||||
model: options.model,
|
||||
permissionMode: options.permissionMode,
|
||||
mcpServers: options.mcpServers,
|
||||
env: options.env,
|
||||
abortController,
|
||||
debug: options.debug,
|
||||
@@ -53,18 +45,14 @@ export function query({
|
||||
authType: options.authType,
|
||||
});
|
||||
|
||||
// Build query options with abortController
|
||||
const queryOptions: QueryOptions = {
|
||||
...options,
|
||||
abortController,
|
||||
};
|
||||
|
||||
// Create Query
|
||||
const queryInstance = new Query(transport, queryOptions, isSingleTurn);
|
||||
|
||||
// Handle prompt based on type
|
||||
if (isSingleTurn) {
|
||||
// For single-turn queries, send the prompt directly via transport
|
||||
const stringPrompt = prompt as string;
|
||||
const message: CLIUserMessage = {
|
||||
type: 'user',
|
||||
@@ -95,16 +83,9 @@ export function query({
|
||||
return queryInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Backward compatibility alias
|
||||
* @deprecated Use query() instead
|
||||
*/
|
||||
export const createQuery = query;
|
||||
|
||||
function validateOptions(
|
||||
options: QueryOptions,
|
||||
): ReturnType<typeof parseExecutableSpec> {
|
||||
// Validate options using Zod schema
|
||||
const validationResult = QueryOptionsSchema.safeParse(options);
|
||||
if (!validationResult.success) {
|
||||
const errors = validationResult.error.errors
|
||||
@@ -113,7 +94,6 @@ function validateOptions(
|
||||
throw new Error(`Invalid QueryOptions: ${errors}`);
|
||||
}
|
||||
|
||||
// Validate executable path early to provide clear error messages
|
||||
let parsedExecutable: ReturnType<typeof parseExecutableSpec>;
|
||||
try {
|
||||
parsedExecutable = parseExecutableSpec(options.pathToQwenExecutable);
|
||||
@@ -122,7 +102,6 @@ function validateOptions(
|
||||
throw new Error(`Invalid pathToQwenExecutable: ${errorMessage}`);
|
||||
}
|
||||
|
||||
// Validate no MCP server name conflicts (cross-field validation not easily expressible in Zod)
|
||||
if (options.mcpServers && options.sdkMcpServers) {
|
||||
const externalNames = Object.keys(options.mcpServers);
|
||||
const sdkNames = Object.keys(options.sdkMcpServers);
|
||||
|
||||
@@ -7,11 +7,6 @@ import { parseJsonLinesStream } from '../utils/jsonLines.js';
|
||||
import { prepareSpawnInfo } from '../utils/cliPath.js';
|
||||
import { AbortError } from '../types/errors.js';
|
||||
|
||||
type ExitListener = {
|
||||
callback: (error?: Error) => void;
|
||||
handler: (code: number | null, signal: NodeJS.Signals | null) => void;
|
||||
};
|
||||
|
||||
export class ProcessTransport implements Transport {
|
||||
private childProcess: ChildProcess | null = null;
|
||||
private childStdin: Writable | null = null;
|
||||
@@ -21,7 +16,6 @@ export class ProcessTransport implements Transport {
|
||||
private _exitError: Error | null = null;
|
||||
private closed = false;
|
||||
private abortController: AbortController;
|
||||
private exitListeners: ExitListener[] = [];
|
||||
private processExitHandler: (() => void) | null = null;
|
||||
private abortHandler: (() => void) | null = null;
|
||||
|
||||
@@ -115,15 +109,6 @@ export class ProcessTransport implements Transport {
|
||||
this.logForDebugging(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
const error = this._exitError;
|
||||
for (const listener of this.exitListeners) {
|
||||
try {
|
||||
listener.callback(error || undefined);
|
||||
} catch (err) {
|
||||
this.logForDebugging(`Exit listener error: ${err}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -192,11 +177,6 @@ export class ProcessTransport implements Transport {
|
||||
this.abortHandler = null;
|
||||
}
|
||||
|
||||
for (const { handler } of this.exitListeners) {
|
||||
this.childProcess?.off('close', handler);
|
||||
}
|
||||
this.exitListeners = [];
|
||||
|
||||
if (this.childProcess && !this.childProcess.killed) {
|
||||
this.childProcess.kill('SIGTERM');
|
||||
setTimeout(() => {
|
||||
@@ -343,30 +323,6 @@ export class ProcessTransport implements Transport {
|
||||
return this._exitError;
|
||||
}
|
||||
|
||||
onExit(callback: (error?: Error) => void): () => void {
|
||||
if (!this.childProcess) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const handler = (code: number | null, signal: NodeJS.Signals | null) => {
|
||||
const error = this.getProcessExitError(code, signal);
|
||||
callback(error);
|
||||
};
|
||||
|
||||
this.childProcess.on('close', handler);
|
||||
this.exitListeners.push({ callback, handler });
|
||||
|
||||
return () => {
|
||||
if (this.childProcess) {
|
||||
this.childProcess.off('close', handler);
|
||||
}
|
||||
const index = this.exitListeners.findIndex((l) => l.handler === handler);
|
||||
if (index !== -1) {
|
||||
this.exitListeners.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
endInput(): void {
|
||||
if (this.childStdin) {
|
||||
this.childStdin.end();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { PermissionMode, PermissionSuggestion } from './protocol.js';
|
||||
import type { ExternalMcpServerConfig } from './queryOptionsSchema.js';
|
||||
|
||||
export type { PermissionMode };
|
||||
|
||||
@@ -23,7 +22,6 @@ export type TransportOptions = {
|
||||
cwd?: string;
|
||||
model?: string;
|
||||
permissionMode?: PermissionMode;
|
||||
mcpServers?: Record<string, ExternalMcpServerConfig>;
|
||||
env?: Record<string, string>;
|
||||
abortController?: AbortController;
|
||||
debug?: boolean;
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
/**
|
||||
* Async iterable queue for streaming messages between producer and consumer.
|
||||
*/
|
||||
|
||||
export class Stream<T> implements AsyncIterable<T> {
|
||||
private returned: (() => void) | undefined;
|
||||
private queue: T[] = [];
|
||||
@@ -24,23 +20,18 @@ export class Stream<T> implements AsyncIterable<T> {
|
||||
}
|
||||
|
||||
async next(): Promise<IteratorResult<T>> {
|
||||
// Check queue first - if there are queued items, return immediately
|
||||
if (this.queue.length > 0) {
|
||||
return Promise.resolve({
|
||||
done: false,
|
||||
value: this.queue.shift()!,
|
||||
});
|
||||
}
|
||||
// Check if stream is done
|
||||
if (this.isDone) {
|
||||
return Promise.resolve({ done: true, value: undefined });
|
||||
}
|
||||
// Check for errors that occurred before next() was called
|
||||
// This ensures errors set via error() before iteration starts are properly rejected
|
||||
if (this.hasError) {
|
||||
return Promise.reject(this.hasError);
|
||||
}
|
||||
// No queued items, not done, no error - set up promise for next value/error
|
||||
return new Promise<IteratorResult<T>>((resolve, reject) => {
|
||||
this.readResolve = resolve;
|
||||
this.readReject = reject;
|
||||
@@ -70,15 +61,12 @@ export class Stream<T> implements AsyncIterable<T> {
|
||||
|
||||
error(error: Error): void {
|
||||
this.hasError = error;
|
||||
// If readReject exists (next() has been called), reject immediately
|
||||
if (this.readReject) {
|
||||
const reject = this.readReject;
|
||||
this.readResolve = undefined;
|
||||
this.readReject = undefined;
|
||||
reject(error);
|
||||
}
|
||||
// Otherwise, error is stored in hasError and will be rejected when next() is called
|
||||
// This handles the case where error() is called before the first next() call
|
||||
}
|
||||
|
||||
return(): Promise<IteratorResult<T>> {
|
||||
|
||||
@@ -154,7 +154,6 @@ export function parseExecutableSpec(executableSpec?: string): {
|
||||
executablePath: string;
|
||||
isExplicitRuntime: boolean;
|
||||
} {
|
||||
// Handle empty string case first (before checking for undefined/null)
|
||||
if (
|
||||
executableSpec === '' ||
|
||||
(executableSpec && executableSpec.trim() === '')
|
||||
@@ -163,7 +162,6 @@ export function parseExecutableSpec(executableSpec?: string): {
|
||||
}
|
||||
|
||||
if (!executableSpec) {
|
||||
// Auto-detect native CLI
|
||||
return {
|
||||
executablePath: findNativeCliPath(),
|
||||
isExplicitRuntime: false,
|
||||
@@ -178,7 +176,6 @@ export function parseExecutableSpec(executableSpec?: string): {
|
||||
throw new Error(`Invalid runtime specification: '${executableSpec}'`);
|
||||
}
|
||||
|
||||
// Validate runtime is supported
|
||||
const supportedRuntimes = ['node', 'bun', 'tsx', 'deno'];
|
||||
if (!supportedRuntimes.includes(runtime)) {
|
||||
throw new Error(
|
||||
@@ -186,7 +183,6 @@ export function parseExecutableSpec(executableSpec?: string): {
|
||||
);
|
||||
}
|
||||
|
||||
// Validate runtime availability
|
||||
if (!validateRuntimeAvailability(runtime)) {
|
||||
throw new Error(
|
||||
`Runtime '${runtime}' is not available on this system. Please install it first.`,
|
||||
@@ -195,7 +191,6 @@ export function parseExecutableSpec(executableSpec?: string): {
|
||||
|
||||
const resolvedPath = path.resolve(filePath);
|
||||
|
||||
// Validate file exists
|
||||
if (!fs.existsSync(resolvedPath)) {
|
||||
throw new Error(
|
||||
`Executable file not found at '${resolvedPath}' for runtime '${runtime}'. ` +
|
||||
@@ -203,7 +198,6 @@ export function parseExecutableSpec(executableSpec?: string): {
|
||||
);
|
||||
}
|
||||
|
||||
// Validate file extension matches runtime
|
||||
if (!validateFileExtensionForRuntime(resolvedPath, runtime)) {
|
||||
const ext = path.extname(resolvedPath);
|
||||
throw new Error(
|
||||
@@ -285,14 +279,6 @@ function getExpectedExtensions(runtime: string): string[] {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use parseExecutableSpec and prepareSpawnInfo instead
|
||||
*/
|
||||
export function resolveCliPath(explicitPath?: string): string {
|
||||
const parsed = parseExecutableSpec(explicitPath);
|
||||
return parsed.executablePath;
|
||||
}
|
||||
|
||||
function detectRuntimeFromExtension(filePath: string): string | undefined {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
|
||||
@@ -356,10 +342,3 @@ export function prepareSpawnInfo(executableSpec?: string): SpawnInfo {
|
||||
originalInput: executableSpec || '',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use prepareSpawnInfo() instead
|
||||
*/
|
||||
export function findCliPath(): string {
|
||||
return findNativeCliPath();
|
||||
}
|
||||
|
||||
@@ -38,20 +38,16 @@ export async function* parseJsonLinesStream(
|
||||
context = 'JsonLines',
|
||||
): AsyncGenerator<unknown, void, unknown> {
|
||||
for await (const line of lines) {
|
||||
// Skip empty lines
|
||||
if (line.trim().length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse with error handling
|
||||
const message = parseJsonLineSafe(line, context);
|
||||
|
||||
// Skip malformed messages
|
||||
if (message === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate message structure
|
||||
if (!isValidMessage(message)) {
|
||||
console.warn(
|
||||
`[${context}] Invalid message structure (missing 'type' field), skipping:`,
|
||||
|
||||
Reference in New Issue
Block a user