diff --git a/sdk/src/query-gsd-tools-runtime.ts b/sdk/src/query-gsd-tools-runtime.ts index 1db59bc82..d58b38e3f 100644 --- a/sdk/src/query-gsd-tools-runtime.ts +++ b/sdk/src/query-gsd-tools-runtime.ts @@ -7,7 +7,7 @@ import { QuerySubprocessAdapter } from './query-subprocess-adapter.js'; import { QueryNativeDirectAdapter } from './query-native-direct-adapter.js'; import { QueryNativeHotpathAdapter } from './query-native-hotpath-adapter.js'; import { formatQueryRawOutput } from './query-raw-output-projection.js'; -import { GSDToolsError } from './gsd-tools-error.js'; +import { failureToolsError, timeoutToolsError } from './query-tools-error-factory.js'; export interface GSDToolsRuntime { registry: ReturnType; @@ -34,18 +34,18 @@ export function createGSDToolsRuntime(opts: { timeoutMs: opts.timeoutMs, workstream: opts.workstream, createTimeoutError: (message, command, args, stderr, timeoutMs) => - GSDToolsError.timeout(message, command, args, stderr, timeoutMs), + timeoutToolsError(message, command, args, stderr, timeoutMs), createFailureError: (message, command, args, exitCode, stderr) => - GSDToolsError.failure(message, command, args, exitCode, stderr), + failureToolsError(message, command, args, exitCode, stderr), }); const nativeDirectAdapter = new QueryNativeDirectAdapter({ timeoutMs: opts.timeoutMs, dispatch: (registryCommand, registryArgs) => registry.dispatch(registryCommand, registryArgs, opts.projectDir), createTimeoutError: (message, command, args) => - GSDToolsError.timeout(message, command, args, '', opts.timeoutMs), + timeoutToolsError(message, command, args, '', opts.timeoutMs), createFailureError: (message, command, args, cause) => - GSDToolsError.failure(message, command, args, 1, '', { cause }), + failureToolsError(message, command, args, 1, '', cause), }); const transport = new GSDTransport(registry, { diff --git a/sdk/src/query-tools-error-factory.test.ts b/sdk/src/query-tools-error-factory.test.ts new file mode 100644 index 000000000..dad1075e6 --- /dev/null +++ b/sdk/src/query-tools-error-factory.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from 'vitest'; +import { ErrorClassification, GSDError } from './errors.js'; +import { + failureToolsError, + timeoutToolsError, + toToolsErrorFromUnknown, +} from './query-tools-error-factory.js'; + +describe('query tools error factory', () => { + it('builds timeout and failure tools errors', () => { + expect(timeoutToolsError('t', 'state', ['load'], '', 10).classification).toEqual({ kind: 'timeout', timeoutMs: 10 }); + expect(failureToolsError('f', 'state', ['load'], 1).classification).toEqual({ kind: 'failure' }); + }); + + it('maps GSDError to failure with semantic exit code', () => { + const err = toToolsErrorFromUnknown('state', ['load'], new GSDError('bad', ErrorClassification.Validation)); + expect(err.exitCode).toBe(10); + expect(err.classification).toEqual({ kind: 'failure' }); + }); + + it('maps timeout-like unknown errors to timeout classification', () => { + const err = toToolsErrorFromUnknown('state', ['load'], new Error('gsd-tools timed out after 50ms: state load')); + expect(err.classification).toEqual({ kind: 'timeout', timeoutMs: 50 }); + }); +}); diff --git a/sdk/src/query-tools-error-factory.ts b/sdk/src/query-tools-error-factory.ts new file mode 100644 index 000000000..838f51381 --- /dev/null +++ b/sdk/src/query-tools-error-factory.ts @@ -0,0 +1,31 @@ +import { GSDError, exitCodeFor } from './errors.js'; +import { GSDToolsError } from './gsd-tools-error.js'; +import { errorMessage, toFailureSignal } from './query-failure-classification.js'; + +export function timeoutToolsError(message: string, command: string, args: string[], stderr = '', timeoutMs?: number): GSDToolsError { + return GSDToolsError.timeout(message, command, args, stderr, timeoutMs); +} + +export function failureToolsError( + message: string, + command: string, + args: string[], + exitCode: number | null, + stderr = '', + cause?: unknown, +): GSDToolsError { + return GSDToolsError.failure(message, command, args, exitCode, stderr, cause === undefined ? undefined : { cause }); +} + +export function toToolsErrorFromUnknown(command: string, args: string[], err: unknown): GSDToolsError { + if (err instanceof GSDError) { + return failureToolsError(err.message, command, args, exitCodeFor(err.classification), '', err); + } + + const msg = errorMessage(err); + const signal = toFailureSignal(err); + if (signal.kind === 'timeout') { + return timeoutToolsError(msg, command, args, '', signal.timeoutMs); + } + return failureToolsError(msg, command, args, 1, '', err instanceof Error ? err : undefined); +} diff --git a/sdk/src/query-tools-error-mapper.ts b/sdk/src/query-tools-error-mapper.ts index 06f54b6eb..835545b6e 100644 --- a/sdk/src/query-tools-error-mapper.ts +++ b/sdk/src/query-tools-error-mapper.ts @@ -1,27 +1,9 @@ -import { GSDError, exitCodeFor } from './errors.js'; -import { GSDToolsError } from './gsd-tools-error.js'; -import { errorMessage, toFailureSignal } from './query-failure-classification.js'; +import { toToolsErrorFromUnknown } from './query-tools-error-factory.js'; +import type { GSDToolsError } from './gsd-tools-error.js'; /** * Module owning projection of internal errors to GSDToolsError contract. */ export function toGSDToolsError(command: string, args: string[], err: unknown): GSDToolsError { - if (err instanceof GSDError) { - return GSDToolsError.failure( - err.message, - command, - args, - exitCodeFor(err.classification), - '', - { cause: err }, - ); - } - - const msg = errorMessage(err); - const signal = toFailureSignal(err); - if (signal.kind === 'timeout') { - return GSDToolsError.timeout(msg, command, args, '', signal.timeoutMs, err instanceof Error ? { cause: err } : undefined); - } - - return GSDToolsError.failure(msg, command, args, 1, '', err instanceof Error ? { cause: err } : undefined); + return toToolsErrorFromUnknown(command, args, err); }